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
@@ -6,95 +6,60 @@ require_relative "operation_transformer"
6
6
  require_relative "constraint_transformer"
7
7
  require_relative "tagged_value_transformer"
8
8
  require_relative "object_property_transformer"
9
- require_relative "generalization_transformer"
10
- require_relative "association_transformer"
9
+ require_relative "stereotype_loader"
10
+ require_relative "generalization_builder"
11
+ require_relative "association_builder"
11
12
  require "lutaml/uml"
12
13
 
13
14
  module Lutaml
14
15
  module Qea
15
16
  module Factory
16
- # Transforms EA objects (Class type) to UML classes
17
17
  class ClassTransformer < BaseTransformer
18
- # Transform EA object to UML class
19
- # @param ea_object [EaObject] EA object model
20
- # @return [Lutaml::Uml::Class] UML class
21
- def transform(ea_object) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
18
+ def transform(ea_object)
22
19
  return nil if ea_object.nil?
23
20
 
24
- # Allow Class, Interface, ProxyConnector, and Text objects
25
21
  is_class_type = ea_object.uml_class? || ea_object.interface?
26
22
  is_proxy = ea_object.object_type == "ProxyConnector"
27
23
  is_text_class = ea_object.object_type == "Text"
28
24
  return nil unless is_class_type || is_proxy || is_text_class
29
25
 
30
- Lutaml::Uml::Class.new.tap do |klass| # rubocop:disable Metrics/BlockLength
31
- # Map basic properties
26
+ Lutaml::Uml::Class.new.tap do |klass|
32
27
  klass.name = ea_object.name
33
28
  klass.xmi_id = normalize_guid_to_xmi_format(ea_object.ea_guid,
34
29
  "EAID")
35
30
  klass.is_abstract = ea_object.abstract?
36
- # Text objects exported as Class in XMI
37
31
  klass.type = "Class"
38
32
  klass.visibility = map_visibility(ea_object.visibility)
39
33
 
40
- # Map stereotype - return string if single, array if multiple
41
- stereotypes = []
42
- if ea_object.stereotype && !ea_object.stereotype.empty?
43
- stereotypes << ea_object.stereotype
44
- end
34
+ stereotypes = build_stereotypes(ea_object)
35
+ klass.stereotype = stereotypes unless stereotypes.empty?
45
36
 
46
- # Check t_xref for additional stereotypes
47
- # (only if not already added)
48
- xref_stereotype = load_stereotype_from_xref(ea_object.ea_guid)
49
- if xref_stereotype && !stereotypes.include?(xref_stereotype)
50
- stereotypes << xref_stereotype
51
- end
52
-
53
- # Assign stereotypes (always as array for collection: true)
54
- unless stereotypes.empty?
55
- klass.stereotype = stereotypes
56
- end
57
-
58
- # Add "interface" stereotype if it's an interface
59
- if ea_object.interface? && !klass.stereotype.include?("interface")
60
- klass.stereotype << "interface"
61
- end
62
-
63
- # Map definition/notes
64
37
  klass.definition = normalize_line_endings(ea_object.note) unless
65
38
  ea_object.note.nil? || ea_object.note.empty?
66
39
 
67
- # Load and transform attributes
40
+ gen_builder = GeneralizationBuilder.new(database)
41
+ assoc_builder = AssociationBuilder.new(database)
42
+
68
43
  attrs = load_attributes(ea_object.ea_object_id)
69
- assoc_attrs = convert_to_top_element_attributes(
70
- load_association_attributes(ea_object.ea_object_id),
44
+ assoc_attrs = gen_builder.convert_to_top_element_attributes(
45
+ assoc_builder.load_association_attributes(ea_object.ea_object_id),
71
46
  )
72
47
  klass.attributes = attrs + assoc_attrs
73
48
 
74
- # Load and transform operations
75
49
  klass.operations = load_operations(ea_object.ea_object_id)
76
-
77
- # Load and transform constraints
78
50
  klass.constraints = load_constraints(ea_object.ea_object_id)
79
-
80
- # Load and transform tagged values
81
51
  klass.tagged_values = load_tagged_values(ea_object.ea_guid)
82
-
83
- # Load and transform object properties (as additional tagged values)
84
52
  klass.tagged_values.concat(
85
53
  load_object_properties(ea_object.ea_object_id),
86
54
  )
87
55
 
88
- # Load generalization (inheritance)
89
- klass.generalization = load_generalization(ea_object.ea_object_id)
90
-
91
- # Load association generalizations
92
- klass.association_generalization = load_association_generalizations(
56
+ klass.generalization = gen_builder.load_generalization(
93
57
  ea_object.ea_object_id,
94
58
  )
59
+ klass.association_generalization = gen_builder
60
+ .load_association_generalizations(ea_object.ea_object_id)
95
61
 
96
- # Load associations for this class
97
- klass.associations = load_class_associations(
62
+ klass.associations = assoc_builder.load_class_associations(
98
63
  ea_object.ea_object_id, ea_object.ea_guid
99
64
  )
100
65
  end
@@ -102,584 +67,69 @@ module Lutaml
102
67
 
103
68
  private
104
69
 
105
- # Load and transform attributes for a class
106
- # @param object_id [Integer] Object ID
107
- # @return [Array<Lutaml::Uml::TopElementAttribute>] UML attributes
70
+ def build_stereotypes(ea_object)
71
+ stereotypes = []
72
+ if ea_object.stereotype && !ea_object.stereotype.empty?
73
+ stereotypes << ea_object.stereotype
74
+ end
75
+
76
+ xref_stereotype = StereotypeLoader.new(database)
77
+ .load_from_xref(ea_object.ea_guid)
78
+ if xref_stereotype && !stereotypes.include?(xref_stereotype)
79
+ stereotypes << xref_stereotype
80
+ end
81
+
82
+ if ea_object.interface? && !stereotypes.include?("interface")
83
+ stereotypes << "interface"
84
+ end
85
+
86
+ stereotypes
87
+ end
88
+
108
89
  def load_attributes(object_id)
109
90
  return [] if object_id.nil?
110
91
 
111
92
  ea_attributes = database.attributes_for_object(object_id)
112
93
  .sort_by { |a| a.pos || 0 }
113
-
114
- attribute_transformer = AttributeTransformer.new(database)
115
- attribute_transformer.transform_collection(ea_attributes)
94
+ AttributeTransformer.new(database).transform_collection(ea_attributes)
116
95
  end
117
96
 
118
- # Load and transform operations for a class
119
- # @param object_id [Integer] Object ID
120
- # @return [Array<Lutaml::Uml::Operation>] UML operations
121
97
  def load_operations(object_id)
122
98
  return [] if object_id.nil?
123
99
 
124
100
  ea_operations = database.operations_for_object(object_id)
125
101
  .sort_by { |op| op.pos || 0 }
126
-
127
- operation_transformer = OperationTransformer.new(database)
128
- operation_transformer.transform_collection(ea_operations)
102
+ OperationTransformer.new(database).transform_collection(ea_operations)
129
103
  end
130
104
 
131
- # Load and transform constraints for a class
132
- # @param object_id [Integer] Object ID
133
- # @return [Array<Lutaml::Uml::Constraint>] UML constraints
134
105
  def load_constraints(object_id)
135
106
  return [] if object_id.nil?
136
107
  return [] unless database.object_constraints
137
108
 
138
- # Filter constraints for this object from the in-memory collection
139
109
  ea_constraints = database.object_constraints.select do |c|
140
110
  c.ea_object_id == object_id
141
111
  end
142
-
143
- # Transform to UML constraints
144
- constraint_transformer = ConstraintTransformer.new(database)
145
- constraint_transformer.transform_collection(ea_constraints)
112
+ ConstraintTransformer.new(database).transform_collection(ea_constraints)
146
113
  end
147
114
 
148
- # Load and transform tagged values for a class
149
- # @param ea_guid [String] Element GUID
150
- # @return [Array<Lutaml::Uml::TaggedValue>] UML tagged values
151
115
  def load_tagged_values(ea_guid)
152
116
  return [] if ea_guid.nil?
153
117
  return [] unless database.tagged_values
154
118
 
155
- # Filter tagged values for this element from the in-memory collection
156
119
  ea_tags = database.tagged_values.select do |tag|
157
120
  tag.element_id == ea_guid
158
121
  end
159
-
160
- # Transform to UML tagged values
161
- tag_transformer = TaggedValueTransformer.new(database)
162
- tag_transformer.transform_collection(ea_tags)
122
+ TaggedValueTransformer.new(database).transform_collection(ea_tags)
163
123
  end
164
124
 
165
- # Load and transform object properties for a class
166
- # @param object_id [Integer] Object ID
167
- # @return [Array<Lutaml::Uml::TaggedValue>] UML tagged values
168
125
  def load_object_properties(object_id)
169
126
  return [] if object_id.nil?
170
127
  return [] unless database.object_properties
171
128
 
172
- # Filter object properties for this object from the in-memory
173
- # collection
174
129
  ea_props = database.object_properties.select do |prop|
175
130
  prop.ea_object_id == object_id
176
131
  end
177
-
178
- # Transform to UML tagged values
179
- prop_transformer = ObjectPropertyTransformer.new(database)
180
- prop_transformer.transform_collection(ea_props)
181
- end
182
-
183
- # Load stereotype from t_xref table
184
- # @param ea_guid [String] Element GUID
185
- # @return [String, nil] Stereotype value
186
- def load_stereotype_from_xref(ea_guid) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
187
- return nil if ea_guid.nil?
188
- return nil unless database.xrefs
189
-
190
- # Find stereotype xref from the in-memory collection
191
- xref = database.xrefs.find do |x|
192
- x.client == ea_guid && x.name == "Stereotypes" &&
193
- x.type == "element property"
194
- end
195
-
196
- return nil unless xref
197
-
198
- # Parse stereotype from Description field
199
- # Format: @STEREO;Name=FeatureType;FQName=...;@ENDSTEREO;
200
- description = xref.description
201
- return nil if description.nil? || description.empty?
202
-
203
- # Extract the Name value from the @STEREO format
204
- if description =~ /@STEREO;Name=([^;]+);/
205
- return $1
206
- end
207
-
208
- nil
209
- end
210
-
211
- # Load generalization for a class
212
- # @param object_id [Integer] Object ID
213
- # @param visited [Set] Set of visited object IDs to prevent
214
- # circular references
215
- # @param is_leaf [Boolean] Whether this is the leaf class
216
- # (not a parent in recursion)
217
- # @return [Lutaml::Uml::Generalization, nil] UML generalization or nil
218
- def load_generalization(object_id, visited = Set.new, is_leaf = true) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity,Style/OptionalBooleanParameter
219
- return nil if object_id.nil?
220
-
221
- # Detect circular reference
222
- if visited.include?(object_id)
223
- warn "Circular inheritance detected for object_id #{object_id}, " \
224
- "stopping recursion"
225
- return nil
226
- end
227
-
228
- # Add current object to visited set
229
- visited = visited.dup.add(object_id)
230
-
231
- # 1. Load CURRENT object
232
- current_obj = find_object_by_id(object_id)
233
- return nil unless current_obj
234
-
235
- # 2. Find generalization connector where this class is the subtype
236
- ea_connector = database.connectors_for_object(object_id)
237
- .find { |c| c.generalization? && c.start_object_id == object_id }
238
-
239
- # 3. Create generalization object for current class
240
- # Even if no parent exists, we need a Generalization
241
- # representing this class
242
- gen_transformer = GeneralizationTransformer.new(database)
243
- generalization = if ea_connector.nil?
244
- # No parent - create terminal generalization
245
- gen_transformer.transform(nil, current_obj)
246
- else
247
- # Has parent - create generalization with parent connector
248
- gen_transformer
249
- .transform(ea_connector, current_obj)
250
- end
251
- return nil unless generalization
252
-
253
- # 4. Load CURRENT object attributes and convert to GeneralAttribute
254
- current_attrs = load_attributes(object_id)
255
- current_assoc_attrs = load_association_attributes(object_id)
256
- general_attrs = convert_to_general_attributes(
257
- current_attrs + current_assoc_attrs,
258
- )
259
-
260
- # Set gen_name and name_ns on general_attributes
261
- # (matches current class context)
262
- upper_klass = generalization.general_upper_klass
263
- gen_name = generalization.general_name
264
- general_attrs.each do |attr|
265
- attr.gen_name = gen_name
266
- # Determine name_ns based on type_ns
267
- # (same logic as transform_general_attributes)
268
- name_ns = case attr.type_ns
269
- when "core", "gml"
270
- upper_klass
271
- else
272
- attr.type_ns
273
- end
274
- attr.name_ns = name_ns || upper_klass
275
- end
276
-
277
- generalization.general_attributes = general_attrs
278
- .sort_by { |a| [a.name.to_s, a.id] }
279
-
280
- # 5. Transform attributes (set name_ns, gen_name) -
281
- # creates working copies
282
- generalization.attributes = transform_general_attributes(
283
- generalization,
284
- )
285
-
286
- # 6. Partition into owned_props and assoc_props
287
- generalization.owned_props = generalization.attributes
288
- .reject(&:has_association)
289
- generalization.assoc_props = generalization.attributes
290
- .select(&:has_association)
291
-
292
- # 7. Recursively load PARENT generalization with circular
293
- # reference detection
294
- # Pass is_leaf=false so parent doesn't populate inherited_props
295
- parent_object_id = ea_connector&.end_object_id
296
- if parent_object_id
297
- parent_gen = load_generalization(parent_object_id, visited, false)
298
- if parent_gen
299
- generalization.general = parent_gen
300
- generalization.has_general = true
301
- end
302
- end
303
-
304
- # 8. Collect inherited properties from ancestor chain
305
- # Only populate inherited_props at the LEAF level
306
- # (matches XMI behavior)
307
- if is_leaf && generalization.has_general
308
- collect_inherited_properties(generalization)
309
- end
310
-
311
- generalization
312
- end
313
-
314
- # Convert TopElementAttribute array to GeneralAttribute array
315
- # @param attributes [Array<Lutaml::Uml::TopElementAttribute>]
316
- # @return [Array<Lutaml::Uml::GeneralAttribute>]
317
- def convert_to_general_attributes(attributes) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
318
- attributes.map do |attr|
319
- Lutaml::Uml::GeneralAttribute.new.tap do |gen_attr|
320
- gen_attr.id = attr.id
321
- gen_attr.name = attr.name
322
- gen_attr.type = attr.type
323
- gen_attr.xmi_id = attr.xmi_id
324
- gen_attr.is_derived = !!attr.is_derived
325
- gen_attr.cardinality = attr.cardinality
326
- gen_attr.definition = attr.definition&.strip
327
- gen_attr.association = attr.association
328
- gen_attr.has_association = !!attr.association
329
- gen_attr.type_ns = attr.type_ns
330
- end
331
- end
332
- end
333
-
334
- # Convert GeneralAttribute array to TopElementAttribute array
335
- # @param attributes [Array<Lutaml::Uml::TopElementAttribute>]
336
- # @return [Array<Lutaml::Uml::TopElementAttribute>]
337
- def convert_to_top_element_attributes(attributes) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
338
- attributes.map do |attr|
339
- Lutaml::Uml::TopElementAttribute.new.tap do |top_attr|
340
- top_attr.id = attr.id
341
- top_attr.name = attr.name
342
- top_attr.type = attr.type
343
- top_attr.xmi_id = attr.xmi_id
344
- top_attr.cardinality = attr.cardinality
345
- top_attr.definition = attr.definition&.strip
346
- top_attr.association = attr.association
347
- top_attr.type_ns = attr.type_ns
348
- top_attr.is_derived = !!attr.is_derived
349
- end
350
- end
351
- end
352
-
353
- # Transform GeneralAttributes with context (name_ns, gen_name)
354
- # Similar to XMI's create_uml_attributes
355
- # @param generalization [Lutaml::Uml::Generalization]
356
- # @return [Array<Lutaml::Uml::GeneralAttribute>]
357
- def transform_general_attributes(generalization) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
358
- upper_klass = generalization.general_upper_klass
359
- gen_name = generalization.general_name
360
- gen_attrs = generalization.general_attributes
361
-
362
- gen_attrs.map do |attr|
363
- # Clone to avoid mutation
364
- transformed = attr.dup
365
-
366
- # Set name_ns based on type_ns
367
- name_ns = case attr.type_ns
368
- when "core", "gml"
369
- upper_klass
370
- else
371
- attr.type_ns
372
- end
373
- name_ns = upper_klass if name_ns.nil?
374
-
375
- transformed.name_ns = name_ns
376
- transformed.gen_name = gen_name
377
- transformed.name = "" if transformed.name.nil?
378
-
379
- transformed
380
- end
381
- end
382
-
383
- # Collect inherited properties from ancestor chain
384
- # Similar to XMI's loop_general_item
385
- # @param generalization [Lutaml::Uml::Generalization]
386
- # @return [void] (modifies generalization in place)
387
- def collect_inherited_properties(generalization) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
388
- inherited_props = []
389
- inherited_assoc_props = []
390
- level = 0
391
-
392
- # Walk the general chain
393
- current_gen = generalization.general
394
- while current_gen
395
- # Set metadata on BOTH general_attributes and attributes
396
- # (matches XMI behavior)
397
- # upper_klass and level are set during the inheritance walk
398
- [current_gen.general_attributes,
399
- current_gen.attributes].each do |attr_list|
400
- attr_list&.each do |attr|
401
- attr.upper_klass = current_gen.general_upper_klass
402
- attr.level = level
403
- end
404
- end
405
-
406
- # Process each attribute in reverse order
407
- # (to show super class first after reversal)
408
- current_gen.attributes.reverse_each do |attr|
409
- # Clone attribute for inherited collection
410
- inherited_attr = attr.dup
411
- inherited_attr.upper_klass = current_gen.general_upper_klass
412
- inherited_attr.gen_name = current_gen.general_name
413
- inherited_attr.level = level
414
-
415
- # Partition by association
416
- if attr.has_association
417
- inherited_assoc_props << inherited_attr
418
- else
419
- inherited_props << inherited_attr
420
- end
421
- end
422
-
423
- # Move to next level
424
- level += 1
425
- current_gen = current_gen.general
426
- end
427
-
428
- # Reverse to show super class first
429
- generalization.inherited_props = inherited_props.reverse
430
- generalization.inherited_assoc_props = inherited_assoc_props.reverse
431
- end
432
-
433
- # Load association generalizations for a class
434
- # @param object_id [Integer] Object ID
435
- # @return [Array<Lutaml::Uml::AssociationGeneralization>]
436
- # UML association generalizations
437
- def load_association_generalizations(object_id) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
438
- return [] if object_id.nil?
439
-
440
- # Find ALL generalization connectors for this class
441
- gen_connectors = database.connectors_for_object(object_id)
442
- .select { |c| c.generalization? && c.start_object_id == object_id }
443
-
444
- gen_connectors.filter_map do |ea_connector|
445
- guid = ea_connector.ea_guid
446
- parent_object_id = ea_connector.end_object_id
447
-
448
- # Find parent object to get its GUID
449
- parent_obj = find_object_by_id(parent_object_id)
450
- next unless parent_obj
451
-
452
- Lutaml::Uml::AssociationGeneralization.new.tap do |ag|
453
- ag.id = normalize_guid_to_xmi_format(guid, "EAID")
454
- ag.type = "uml:Generalization"
455
- ag.general = normalize_guid_to_xmi_format(parent_obj.ea_guid,
456
- "EAID")
457
- end
458
- end
459
- end
460
-
461
- # Load associations for a class
462
- # Creates Association objects from navigable association ends
463
- # (ownedAttributes with association markers)
464
- # This matches XMI behavior where current class is always the owner
465
- # @param object_id [Integer] Object ID
466
- # @param object_guid [String] Object GUID
467
- # @return [Array<Lutaml::Uml::Association>] UML associations
468
- def load_class_associations(object_id, object_guid) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
469
- return [] if object_id.nil?
470
-
471
- associations = []
472
- normalized_owner_xmi_id = normalize_guid_to_xmi_format(object_guid,
473
- "EAID")
474
-
475
- assoc_types = ["Association", "Aggregation", "Composition"].freeze
476
- assoc_connectors = database.connectors_for_object(object_id)
477
- .select { |c| assoc_types.include?(c.connector_type) }
478
-
479
- assoc_connectors.each do |ea_connector| # rubocop:disable Metrics/BlockLength
480
- # Determine which end this class is on
481
- is_start = ea_connector.start_object_id == object_id
482
-
483
- # Get role name (this is the ownedAttribute name)
484
- owner_end_attribute_name = if is_start
485
- ea_connector.destrole
486
- else
487
- ea_connector.sourcerole
488
- end
489
-
490
- # Only create association if there's a navigable role name
491
- # (matches XMI ownedAttribute[@association])
492
- if owner_end_attribute_name.nil? || owner_end_attribute_name.empty?
493
- next
494
- end
495
-
496
- # Get member end (the other class)
497
- member_obj = if is_start
498
- find_object_by_id(ea_connector.end_object_id)
499
- else
500
- find_object_by_id(ea_connector.start_object_id)
501
- end
502
- next unless member_obj
503
-
504
- # Get member end attribute name
505
- # (role at the opposite end, or class name if no role)
506
- member_end_attribute_name = if is_start
507
- ea_connector.sourcerole
508
- else
509
- ea_connector.destrole
510
- end
511
- if member_end_attribute_name.nil? ||
512
- member_end_attribute_name.empty?
513
- member_end_attribute_name = member_obj.name
514
- end
515
-
516
- # Get cardinality for this end
517
- member_cardinality_str = if is_start
518
- ea_connector.destcard
519
- else
520
- ea_connector.sourcecard
521
- end
522
-
523
- # Create association from this class's perspective
524
- associations << Lutaml::Uml::Association.new.tap do |assoc| # rubocop:disable Metrics/BlockLength
525
- assoc.xmi_id = normalize_guid_to_xmi_format(ea_connector.ea_guid,
526
- "EAID")
527
- unless ea_connector.name.nil? || ea_connector.name.empty?
528
- assoc.name = ea_connector.name
529
- end
530
-
531
- # Owner is always the current class (matches XMI)
532
- assoc.owner_end = find_object_by_id(object_id)&.name
533
- assoc.owner_end_xmi_id = normalized_owner_xmi_id
534
- assoc.owner_end_attribute_name = owner_end_attribute_name
535
-
536
- # Member is the other end
537
- assoc.member_end = member_obj.name
538
- assoc.member_end_xmi_id = normalize_guid_to_xmi_format(
539
- member_obj.ea_guid, "EAID"
540
- )
541
- assoc.member_end_attribute_name = member_end_attribute_name
542
-
543
- # Set member_end_type based on connector type
544
- ea_type = ea_connector.connector_type
545
- assoc.member_end_type = ea_type&.downcase
546
-
547
- # Set cardinality
548
- if member_cardinality_str && !member_cardinality_str.empty?
549
- parsed = parse_cardinality(member_cardinality_str)
550
- if parsed[:min] || parsed[:max]
551
- assoc.member_end_cardinality = Lutaml::Uml::Cardinality.new
552
- .tap do |card|
553
- card.min = parsed[:min]
554
- card.max = parsed[:max]
555
- end
556
- end
557
- end
558
-
559
- # Note: XMI does not include connector notes
560
- # in Association definition
561
- end
562
- end
563
-
564
- associations.compact
565
- end
566
-
567
- # Load association-based attributes
568
- # (navigable association ends with role names)
569
- # @param object_id [Integer] Object ID
570
- # @return [Array<Lutaml::Uml::TopElementAttribute>]
571
- # Association-based attributes
572
- def load_association_attributes(object_id) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
573
- return [] if object_id.nil?
574
-
575
- attributes = []
576
-
577
- assoc_types = ["Association", "Aggregation", "Composition"].freeze
578
- assoc_connectors = database.connectors_for_object(object_id)
579
- .select { |c| assoc_types.include?(c.connector_type) }
580
- obj = find_object_by_id(object_id)
581
- obj_pkg_name = find_package_name(obj&.package_id)
582
-
583
- assoc_connectors.each do |ea_connector| # rubocop:disable Metrics/BlockLength
584
- # Check if this object is the source (owner) or target (member)
585
- if ea_connector.start_object_id == object_id
586
- # This class is the source - check for dest role
587
- next if ea_connector.destrole.nil? || ea_connector.destrole.empty?
588
-
589
- target_obj = find_object_by_id(ea_connector.end_object_id)
590
- next unless target_obj
591
-
592
- target_obj_pkg_name = find_package_name(target_obj.package_id)
593
-
594
- attributes << create_association_attribute(
595
- name: ea_connector.destrole,
596
- type: target_obj.name,
597
- type_xmi_id: target_obj.ea_guid,
598
- association_xmi_id: ea_connector.ea_guid,
599
- cardinality: ea_connector.destcard,
600
- definition: ea_connector.notes,
601
- gen_name: obj.name,
602
- name_ns: obj_pkg_name,
603
- type_ns: target_obj_pkg_name,
604
- is_src: false,
605
- )
606
- elsif ea_connector.end_object_id == object_id
607
- # This class is the target - check for source role
608
- if ea_connector.sourcerole.nil? || ea_connector.sourcerole.empty?
609
- next
610
- end
611
-
612
- source_obj = find_object_by_id(ea_connector.start_object_id)
613
- next unless source_obj
614
-
615
- source_obj_pkg_name = find_package_name(source_obj.package_id)
616
-
617
- attributes << create_association_attribute(
618
- name: ea_connector.sourcerole,
619
- type: source_obj.name,
620
- type_xmi_id: source_obj.ea_guid,
621
- association_xmi_id: ea_connector.ea_guid,
622
- cardinality: ea_connector.sourcecard,
623
- definition: ea_connector.notes,
624
- gen_name: obj.name,
625
- name_ns: obj_pkg_name,
626
- type_ns: source_obj_pkg_name,
627
- )
628
- end
629
- end
630
-
631
- attributes.compact
632
- end
633
-
634
- # Create an attribute from association end
635
- # @param name [String] Attribute name (role name)
636
- # @param type [String] Attribute type (class name)
637
- # @param type_xmi_id [String] Type XMI ID
638
- # @param association_xmi_id [String] Association XMI ID
639
- # @param cardinality [String] Cardinality string
640
- # @param gen_name [String] Name of the generalization object
641
- # @return [Lutaml::Uml::GeneralAttribute] Created attribute
642
- def create_association_attribute( # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/ParameterLists
643
- name:, type:, type_xmi_id:,
644
- association_xmi_id:, cardinality:, definition:,
645
- gen_name:, name_ns:, type_ns:, is_src: true
646
- )
647
- Lutaml::Uml::GeneralAttribute.new.tap do |attr|
648
- attr.name = name
649
- attr.type = type
650
- attr.gen_name = gen_name
651
- attr.definition = definition
652
- attr.xmi_id = normalize_guid_to_xmi_format(type_xmi_id, "EAID")
653
- attr.association = normalize_guid_to_xmi_format(
654
- association_xmi_id, "EAID"
655
- )
656
- attr.has_association = true
657
- attr.id = normalize_guid_to_xmi_src_dst_format(
658
- association_xmi_id, "EAID", is_src
659
- )
660
- attr.name_ns = name_ns
661
- attr.type_ns = type_ns
662
-
663
- # Map cardinality if present
664
- if cardinality && !cardinality.empty?
665
- parsed = parse_cardinality(cardinality)
666
- if parsed[:min] || parsed[:max]
667
- attr.cardinality = Lutaml::Uml::Cardinality.new.tap do |card|
668
- card.min = parsed[:min]
669
- card.max = parsed[:max]
670
- end
671
- end
672
- end
673
- end
674
- end
675
-
676
- # Find package name by ID
677
- # @param package_id [Integer] Package ID
678
- # @return [String, Nil] Package name or nil
679
- def find_package_name(package_id)
680
- return nil if package_id.nil?
681
-
682
- database.find_package(package_id)&.name
132
+ ObjectPropertyTransformer.new(database).transform_collection(ea_props)
683
133
  end
684
134
  end
685
135
  end