ea 0.1.0 → 0.1.5

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 (232) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +125 -0
  3. data/Rakefile +12 -4
  4. data/TODO.next/00-publish-blocking-bugs.md +74 -0
  5. data/TODO.next/01-standalone-ea-gem-identity.md +76 -0
  6. data/TODO.next/02-optional-lutaml-uml-dependency.md +47 -0
  7. data/TODO.next/03-slim-lutaml-uml.md +79 -0
  8. data/TODO.next/04-loader-registry-for-uml-repository.md +49 -0
  9. data/TODO.next/05-extract-shared-transformer-methods.md +14 -0
  10. data/TODO.next/06-deduplicate-stereotype-loading.md +17 -0
  11. data/TODO.next/07-transformer-registry-in-factory.md +20 -0
  12. data/TODO.next/08-connector-type-registry.md +27 -0
  13. data/TODO.next/09-element-renderer-registry.md +29 -0
  14. data/TODO.next/10-connector-renderer-lsp.md +18 -0
  15. data/TODO.next/11-consolidate-style-knowledge.md +33 -0
  16. data/TODO.next/12-data-driven-from-db-row.md +24 -0
  17. data/TODO.next/13-extract-duplicated-methods.md +17 -0
  18. data/TODO.next/14-remove-dead-code.md +10 -0
  19. data/TODO.next/15-narrow-exception-handling.md +39 -0
  20. data/TODO.next/16-repository-indexes.md +28 -0
  21. data/TODO.next/17-fix-spec-quality-and-coverage.md +32 -0
  22. data/TODO.next/18-xmi-tool-specific-parser-architecture.md +172 -0
  23. data/TODO.next/19-fix-ea-gemspec-dependency-declarations.md +56 -0
  24. data/TODO.next/20-ci-requires-unreleased-lutaml-uml.md +63 -0
  25. data/TODO.next/21-qeatoxmi-via-xmi-gem.md +340 -0
  26. data/TODO.next/22-strip-respond-to-from-qeatoxmi-specs.md +32 -0
  27. data/TODO.next/23-cleanup-idallocator.md +41 -0
  28. data/TODO.next/24-tighten-parity-specs.md +42 -0
  29. data/TODO.next/25-sparx-eaid-format-for-synthesized-ids.md +62 -0
  30. data/TODO.next/26-fix-uppervalue-lowervalue-count-gap.md +51 -0
  31. data/TODO.next/27-extract-cardinality-module.md +68 -0
  32. data/TODO.next/28-extract-xml-sanitizer.md +51 -0
  33. data/TODO.next/29-ocp-registry-for-classifier-builders.md +58 -0
  34. data/TODO.next/30-struct-return-for-association-end.md +37 -0
  35. data/TODO.next/31-idallocator-specs.md +27 -0
  36. data/TODO.next/32-phase2-gap-sentinel-specs.md +53 -0
  37. data/TODO.next/33-normalize-lower-cleanup.md +30 -0
  38. data/TODO.next/34-document-member-end-order-rt-prefix.md +29 -0
  39. data/TODO.next/35-walk-runstate-for-instance-slots.md +76 -0
  40. data/TODO.next/36-wire-interface-realization.md +50 -0
  41. data/TODO.next/37-visibility-returns-real-booleans.md +36 -0
  42. data/TODO.next/38-xmi-parser-feature-parity.md +75 -0
  43. data/TODO.next/39-ea-spa-cli-command.md +53 -0
  44. data/TODO.next/40-drop-lur-only-check-in-diagrams-extract.md +56 -0
  45. data/config/diagram_styles.yml +200 -0
  46. data/config/model_transformations.yml +266 -0
  47. data/config/qea_schema.yml +1024 -0
  48. data/docs/ea_to_uml_type_mapping.md +89 -0
  49. data/docs/xmi_qea_conversion_capabilities.md +99 -0
  50. data/examples/lur/20251010_current_plateau_v5.1.lur +0 -0
  51. data/examples/lur/basic.lur +0 -0
  52. data/examples/lur/test-output.lur +0 -0
  53. data/examples/lur/test.lur +0 -0
  54. data/examples/lur_basic_usage.rb +221 -0
  55. data/examples/lur_cli_workflow.rb +263 -0
  56. data/examples/lur_statistics.rb +326 -0
  57. data/examples/qea/20251010_current_plateau_v5.1.qea +0 -0
  58. data/examples/qea/ArcGISWorkspace_template.qea +0 -0
  59. data/examples/qea/README_qea_parser.adoc +230 -0
  60. data/examples/qea/UmlModel_template.qea +0 -0
  61. data/examples/qea/basic.qea +0 -0
  62. data/examples/qea/simple.qea +0 -0
  63. data/examples/qea/simple_example.qea +0 -0
  64. data/examples/qea/test.qea +0 -0
  65. data/examples/qea_standalone_query.rb +73 -0
  66. data/examples/qea_to_repository.rb +51 -0
  67. data/examples/smoke_test_real_qea.rb +81 -0
  68. data/exe/ea +7 -0
  69. data/lib/ea/cli/app.rb +81 -0
  70. data/lib/ea/cli/command/base.rb +80 -0
  71. data/lib/ea/cli/command/convert.rb +62 -0
  72. data/lib/ea/cli/command/diagrams.rb +75 -0
  73. data/lib/ea/cli/command/list.rb +61 -0
  74. data/lib/ea/cli/command/parse.rb +29 -0
  75. data/lib/ea/cli/command/repository_builder.rb +64 -0
  76. data/lib/ea/cli/command/spa.rb +79 -0
  77. data/lib/ea/cli/command/stats.rb +20 -0
  78. data/lib/ea/cli/command/validate.rb +41 -0
  79. data/lib/ea/cli/command.rb +17 -0
  80. data/lib/ea/cli/error.rb +34 -0
  81. data/lib/ea/cli/output/formatter.rb +34 -0
  82. data/lib/ea/cli/output/json_formatter.rb +20 -0
  83. data/lib/ea/cli/output/table_formatter.rb +42 -0
  84. data/lib/ea/cli/output/yaml_formatter.rb +20 -0
  85. data/lib/ea/cli/output.rb +56 -0
  86. data/lib/ea/cli.rb +17 -0
  87. data/lib/ea/diagram/configuration.rb +379 -0
  88. data/lib/ea/diagram/element_renderers/base_renderer.rb +77 -0
  89. data/lib/ea/diagram/element_renderers/class_renderer.rb +323 -0
  90. data/lib/ea/diagram/element_renderers/connector_renderer.rb +41 -0
  91. data/lib/ea/diagram/element_renderers/package_renderer.rb +61 -0
  92. data/lib/ea/diagram/element_renderers.rb +43 -0
  93. data/lib/ea/diagram/extractor.rb +560 -0
  94. data/lib/ea/diagram/layout_engine.rb +170 -0
  95. data/lib/ea/diagram/path_builder.rb +202 -0
  96. data/lib/ea/diagram/style_parser.rb +42 -0
  97. data/lib/ea/diagram/style_resolver.rb +276 -0
  98. data/lib/ea/diagram/svg_renderer.rb +274 -0
  99. data/lib/ea/diagram/util.rb +73 -0
  100. data/lib/ea/diagram.rb +47 -0
  101. data/lib/ea/qea/benchmark.rb +210 -0
  102. data/lib/ea/qea/database.rb +308 -0
  103. data/lib/ea/qea/factory/association_builder.rb +203 -0
  104. data/lib/ea/qea/factory/association_transformer.rb +91 -0
  105. data/lib/ea/qea/factory/attribute_tag_transformer.rb +57 -0
  106. data/lib/ea/qea/factory/attribute_transformer.rb +93 -0
  107. data/lib/ea/qea/factory/base_transformer.rb +177 -0
  108. data/lib/ea/qea/factory/class_transformer.rb +116 -0
  109. data/lib/ea/qea/factory/constraint_transformer.rb +75 -0
  110. data/lib/ea/qea/factory/data_type_transformer.rb +77 -0
  111. data/lib/ea/qea/factory/diagram_transformer.rb +157 -0
  112. data/lib/ea/qea/factory/document_builder.rb +283 -0
  113. data/lib/ea/qea/factory/ea_to_uml_factory.rb +229 -0
  114. data/lib/ea/qea/factory/enum_transformer.rb +74 -0
  115. data/lib/ea/qea/factory/generalization_builder.rb +227 -0
  116. data/lib/ea/qea/factory/generalization_transformer.rb +98 -0
  117. data/lib/ea/qea/factory/instance_transformer.rb +68 -0
  118. data/lib/ea/qea/factory/object_property_transformer.rb +58 -0
  119. data/lib/ea/qea/factory/operation_transformer.rb +66 -0
  120. data/lib/ea/qea/factory/package_transformer.rb +145 -0
  121. data/lib/ea/qea/factory/reference_resolver.rb +99 -0
  122. data/lib/ea/qea/factory/stereotype_loader.rb +39 -0
  123. data/lib/ea/qea/factory/tagged_value_transformer.rb +38 -0
  124. data/lib/ea/qea/factory/transformer_registry.rb +80 -0
  125. data/lib/ea/qea/factory.rb +37 -0
  126. data/lib/ea/qea/file_detector.rb +178 -0
  127. data/lib/ea/qea/infrastructure/database_connection.rb +100 -0
  128. data/lib/ea/qea/infrastructure/schema_reader.rb +136 -0
  129. data/lib/ea/qea/infrastructure/table_reader.rb +224 -0
  130. data/lib/ea/qea/infrastructure.rb +12 -0
  131. data/lib/ea/qea/models/base_model.rb +59 -0
  132. data/lib/ea/qea/models/ea_attribute.rb +109 -0
  133. data/lib/ea/qea/models/ea_attribute_tag.rb +100 -0
  134. data/lib/ea/qea/models/ea_complexity_type.rb +79 -0
  135. data/lib/ea/qea/models/ea_connector.rb +160 -0
  136. data/lib/ea/qea/models/ea_connector_type.rb +60 -0
  137. data/lib/ea/qea/models/ea_constraint_type.rb +63 -0
  138. data/lib/ea/qea/models/ea_datatype.rb +104 -0
  139. data/lib/ea/qea/models/ea_diagram.rb +115 -0
  140. data/lib/ea/qea/models/ea_diagram_link.rb +78 -0
  141. data/lib/ea/qea/models/ea_diagram_object.rb +73 -0
  142. data/lib/ea/qea/models/ea_diagram_type.rb +56 -0
  143. data/lib/ea/qea/models/ea_document.rb +63 -0
  144. data/lib/ea/qea/models/ea_object.rb +223 -0
  145. data/lib/ea/qea/models/ea_object_constraint.rb +53 -0
  146. data/lib/ea/qea/models/ea_object_property.rb +87 -0
  147. data/lib/ea/qea/models/ea_object_type.rb +73 -0
  148. data/lib/ea/qea/models/ea_operation.rb +127 -0
  149. data/lib/ea/qea/models/ea_operation_param.rb +76 -0
  150. data/lib/ea/qea/models/ea_package.rb +78 -0
  151. data/lib/ea/qea/models/ea_script.rb +62 -0
  152. data/lib/ea/qea/models/ea_status_type.rb +66 -0
  153. data/lib/ea/qea/models/ea_stereotype.rb +57 -0
  154. data/lib/ea/qea/models/ea_tagged_value.rb +99 -0
  155. data/lib/ea/qea/models/ea_xref.rb +165 -0
  156. data/lib/ea/qea/models.rb +35 -0
  157. data/lib/ea/qea/repositories/base_repository.rb +225 -0
  158. data/lib/ea/qea/repositories/object_repository.rb +219 -0
  159. data/lib/ea/qea/repositories.rb +10 -0
  160. data/lib/ea/qea/services/configuration.rb +211 -0
  161. data/lib/ea/qea/services/database_loader.rb +191 -0
  162. data/lib/ea/qea/services.rb +10 -0
  163. data/lib/ea/qea/validation/association_validator.rb +73 -0
  164. data/lib/ea/qea/validation/attribute_validator.rb +91 -0
  165. data/lib/ea/qea/validation/base_validator.rb +331 -0
  166. data/lib/ea/qea/validation/class_validator.rb +121 -0
  167. data/lib/ea/qea/validation/database/circular_reference_validator.rb +109 -0
  168. data/lib/ea/qea/validation/database/orphan_validator.rb +153 -0
  169. data/lib/ea/qea/validation/database/referential_integrity_validator.rb +128 -0
  170. data/lib/ea/qea/validation/database.rb +16 -0
  171. data/lib/ea/qea/validation/diagram_validator.rb +112 -0
  172. data/lib/ea/qea/validation/formatters/json_formatter.rb +137 -0
  173. data/lib/ea/qea/validation/formatters/text_formatter.rb +235 -0
  174. data/lib/ea/qea/validation/formatters.rb +12 -0
  175. data/lib/ea/qea/validation/operation_validator.rb +71 -0
  176. data/lib/ea/qea/validation/package_validator.rb +111 -0
  177. data/lib/ea/qea/validation/validation_engine.rb +387 -0
  178. data/lib/ea/qea/validation/validation_message.rb +144 -0
  179. data/lib/ea/qea/validation/validation_result.rb +210 -0
  180. data/lib/ea/qea/validation/validator_registry.rb +134 -0
  181. data/lib/ea/qea/validation.rb +28 -0
  182. data/lib/ea/qea/verification/comparison_result.rb +264 -0
  183. data/lib/ea/qea/verification/document_normalizer.rb +169 -0
  184. data/lib/ea/qea/verification/document_verifier.rb +322 -0
  185. data/lib/ea/qea/verification/element_comparator.rb +277 -0
  186. data/lib/ea/qea/verification/structure_matcher.rb +287 -0
  187. data/lib/ea/qea/verification.rb +14 -0
  188. data/lib/ea/qea.rb +185 -0
  189. data/lib/ea/transformations/configuration.rb +333 -0
  190. data/lib/ea/transformations/format_registry.rb +366 -0
  191. data/lib/ea/transformations/parsers/base_parser.rb +482 -0
  192. data/lib/ea/transformations/parsers/qea_parser.rb +401 -0
  193. data/lib/ea/transformations/parsers/xmi_parser.rb +243 -0
  194. data/lib/ea/transformations/transformation_engine.rb +390 -0
  195. data/lib/ea/transformations.rb +85 -0
  196. data/lib/ea/transformers/qea_to_xmi/association_end.rb +19 -0
  197. data/lib/ea/transformers/qea_to_xmi/cardinality.rb +96 -0
  198. data/lib/ea/transformers/qea_to_xmi/context.rb +106 -0
  199. data/lib/ea/transformers/qea_to_xmi/guid_format.rb +56 -0
  200. data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +92 -0
  201. data/lib/ea/transformers/qea_to_xmi/run_state.rb +107 -0
  202. data/lib/ea/transformers/qea_to_xmi/transformer.rb +607 -0
  203. data/lib/ea/transformers/qea_to_xmi/visibility.rb +73 -0
  204. data/lib/ea/transformers/qea_to_xmi.rb +29 -0
  205. data/lib/ea/transformers/uml_to_xmi/id_generator.rb +54 -0
  206. data/lib/ea/transformers/uml_to_xmi/transformer.rb +152 -0
  207. data/lib/ea/transformers/uml_to_xmi/writer.rb +96 -0
  208. data/lib/ea/transformers/uml_to_xmi.rb +16 -0
  209. data/lib/ea/transformers.rb +34 -0
  210. data/lib/ea/version.rb +1 -1
  211. data/lib/ea/xmi/liquid_drops/association_drop.rb +56 -0
  212. data/lib/ea/xmi/liquid_drops/attribute_drop.rb +72 -0
  213. data/lib/ea/xmi/liquid_drops/cardinality_drop.rb +35 -0
  214. data/lib/ea/xmi/liquid_drops/connector_drop.rb +54 -0
  215. data/lib/ea/xmi/liquid_drops/constraint_drop.rb +29 -0
  216. data/lib/ea/xmi/liquid_drops/data_type_drop.rb +63 -0
  217. data/lib/ea/xmi/liquid_drops/dependency_drop.rb +36 -0
  218. data/lib/ea/xmi/liquid_drops/diagram_drop.rb +34 -0
  219. data/lib/ea/xmi/liquid_drops/enum_drop.rb +49 -0
  220. data/lib/ea/xmi/liquid_drops/enum_owned_literal_drop.rb +25 -0
  221. data/lib/ea/xmi/liquid_drops/generalization_attribute_drop.rb +87 -0
  222. data/lib/ea/xmi/liquid_drops/generalization_drop.rb +127 -0
  223. data/lib/ea/xmi/liquid_drops/klass_drop.rb +191 -0
  224. data/lib/ea/xmi/liquid_drops/operation_drop.rb +29 -0
  225. data/lib/ea/xmi/liquid_drops/package_drop.rb +108 -0
  226. data/lib/ea/xmi/liquid_drops/root_drop.rb +34 -0
  227. data/lib/ea/xmi/liquid_drops/source_target_drop.rb +43 -0
  228. data/lib/ea/xmi/lookup_service.rb +89 -0
  229. data/lib/ea/xmi/parser.rb +1015 -0
  230. data/lib/ea/xmi.rb +35 -0
  231. data/lib/ea.rb +10 -1
  232. metadata +387 -9
@@ -0,0 +1,1015 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "lutaml/path"
5
+
6
+ module Ea
7
+ module Xmi
8
+ # Parses EA XMI files into Lutaml::Uml::Document objects.
9
+ #
10
+ # Consolidates what was previously 6 separate modules
11
+ # (Xml, XmiBase, XmiConnector, XmiClassMembers, XmiToUml,
12
+ # XmiToUmlGeneralization) into a single coherent class.
13
+ #
14
+ # The XMI format handled here is EA/Sparx-specific.
15
+ class Parser
16
+ class << self
17
+ # Parse an XMI file into a UML Document.
18
+ def parse(xml)
19
+ new.parse(get_xmi_model(xml))
20
+ end
21
+
22
+ # Parse XMI and serialize to Liquid drops for template rendering.
23
+ def serialize_to_liquid(xml, guidance = nil)
24
+ new.serialize_to_liquid(get_xmi_model(xml), guidance)
25
+ end
26
+
27
+ private
28
+
29
+ def get_xmi_model(xml)
30
+ ::Xmi::Sparx::Root.parse_xml(File.read(xml))
31
+ end
32
+ end
33
+
34
+ # Public instance methods
35
+
36
+ def parse(xmi_model)
37
+ setup_model(xmi_model)
38
+ build_document(xmi_model)
39
+ end
40
+
41
+ def serialize_to_liquid(xmi_model, guidance = nil)
42
+ setup_model(xmi_model)
43
+ document = build_document(xmi_model)
44
+ lookup = LookupService.new(self)
45
+ options = {
46
+ xmi_root_model: @xmi_root_model,
47
+ id_name_mapping: @id_name_mapping,
48
+ lookup: lookup,
49
+ with_gen: true,
50
+ with_absolute_path: true,
51
+ }
52
+ LiquidDrops::RootDrop.new(document, guidance, options)
53
+ end
54
+
55
+ # Public read access for collaborators (LookupService)
56
+ attr_reader :xmi_root_model
57
+
58
+ def xmi_index
59
+ if @xmi_index.nil? && @xmi_root_model
60
+ @xmi_index = @xmi_root_model.index
61
+ @id_name_mapping ||= @xmi_index.id_name_map
62
+ end
63
+ @xmi_index
64
+ end
65
+
66
+ def id_name_mapping
67
+ @id_name_mapping
68
+ end
69
+
70
+ # Public lookup methods used by LookupService and Liquid drops
71
+
72
+ def fetch_connector(link_id)
73
+ xmi_index.find_connector(link_id)
74
+ end
75
+
76
+ def fetch_definition_node_value(link_id, node_name)
77
+ connector_node = fetch_connector(link_id)
78
+ return nil unless connector_node
79
+
80
+ node = connector_node.public_send(node_name.to_sym)
81
+ return nil unless node
82
+
83
+ documentation = node.documentation
84
+ if documentation.is_a?(::Xmi::Sparx::Element::Documentation)
85
+ documentation&.value
86
+ else
87
+ documentation
88
+ end
89
+ end
90
+
91
+ def get_package_name(package) # rubocop:disable Metrics/AbcSize
92
+ return package.name unless package.name.nil?
93
+
94
+ connector = fetch_connector(package.id)
95
+ if connector.target&.model&.name
96
+ return "#{connector.target.model.name} (#{package.type.split(':').last})"
97
+ end
98
+
99
+ "unnamed"
100
+ end
101
+
102
+ def find_packaged_element_by_id(id)
103
+ xmi_index.find_packaged_element(id)
104
+ end
105
+
106
+ def find_upper_level_packaged_element(klass_id)
107
+ xmi_index.find_parent(klass_id)
108
+ end
109
+
110
+ def find_subtype_of_from_owned_attribute_type(id) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
111
+ @pkg_elements_owned_attributes ||= begin
112
+ cache = {}
113
+ all_packaged_elements.each do |e|
114
+ next unless e.owned_attribute
115
+
116
+ e.owned_attribute.each do |oa|
117
+ next unless oa.association && oa.uml_type && oa.uml_type.idref
118
+ cache[oa.uml_type.idref] = e.name
119
+ end
120
+ end
121
+ cache
122
+ end
123
+
124
+ @pkg_elements_owned_attributes[id]
125
+ end
126
+
127
+ def find_subtype_of_from_generalization(id) # rubocop:disable Metrics/AbcSize,Metrics:CyclomaticComplexity,Metrics:MethodLength,Metrics:PerceivedComplexity
128
+ matched_element = xmi_index.find_element(id)
129
+ return unless matched_element&.links&.any?
130
+
131
+ matched_generalization = nil
132
+ matched_element.links.each do |link|
133
+ matched_generalization = link&.generalization&.find { |g| g.start == id }
134
+ break if matched_generalization
135
+ end
136
+
137
+ return if matched_generalization&.end.nil?
138
+ lookup_entity_name(matched_generalization.end)
139
+ end
140
+
141
+ def find_klass_packaged_element(path)
142
+ lutaml_path = Lutaml::Path.parse(path)
143
+ if lutaml_path.segments.one?
144
+ return find_klass_packaged_element_by_name(path)
145
+ end
146
+
147
+ find_klass_packaged_element_by_path(lutaml_path)
148
+ end
149
+
150
+ def find_klass_packaged_element_by_name(name)
151
+ xmi_index.find_packaged_by_name_and_types(name, ["uml:Class", "uml:AssociationClass"])
152
+ end
153
+
154
+ def find_enum_packaged_element_by_name(name)
155
+ xmi_index.packaged_elements_of_type("uml:Enumeration").find { |e| e.name == name }
156
+ end
157
+
158
+ def select_dependencies_by_supplier(supplier_id)
159
+ xmi_index.packaged_elements_of_type("uml:Dependency").select { |e| e.supplier == supplier_id }
160
+ end
161
+
162
+ def select_dependencies_by_client(client_id)
163
+ xmi_index.packaged_elements_of_type("uml:Dependency").select { |e| e.client == client_id }
164
+ end
165
+
166
+ def find_packaged_element_by_name(name)
167
+ xmi_index.packaged_elements.find { |e| e.name == name }
168
+ end
169
+
170
+ def doc_node_attribute_value(node_id, attr_name)
171
+ doc_node = fetch_element(node_id)
172
+ return unless doc_node
173
+
174
+ doc_node.properties&.public_send(
175
+ Lutaml::Model::Utils.snake_case(attr_name).to_sym
176
+ )
177
+ end
178
+
179
+ def lookup_attribute_documentation(xmi_id)
180
+ attribute_node = fetch_attribute_node(xmi_id)
181
+ return unless attribute_node&.documentation
182
+
183
+ attribute_node.documentation.value
184
+ end
185
+
186
+ def lookup_element_prop_documentation(xmi_id)
187
+ element_node = xmi_index.find_element(xmi_id)
188
+ return unless element_node&.properties
189
+
190
+ element_node.properties.documentation
191
+ end
192
+
193
+ def lookup_entity_name(xmi_id)
194
+ @id_name_mapping[xmi_id]
195
+ end
196
+
197
+ def lookup_assoc_def(association)
198
+ connector = fetch_connector(association)
199
+ connector&.documentation&.value
200
+ end
201
+
202
+ def get_ns_by_xmi_id(xmi_id)
203
+ return unless xmi_id
204
+
205
+ p = find_packaged_element_by_id(xmi_id)
206
+ return unless p
207
+
208
+ find_upper_level_packaged_element(p.id)&.name
209
+ end
210
+
211
+ def fetch_element(klass_id)
212
+ xmi_index.find_element(klass_id)
213
+ end
214
+
215
+ def fetch_attribute_node(xmi_id)
216
+ xmi_index.find_attribute(xmi_id)
217
+ end
218
+
219
+ def all_packaged_elements
220
+ xmi_index.packaged_elements
221
+ end
222
+
223
+ def select_all_packaged_elements(all_elements, model, type)
224
+ select_all_items(all_elements, model, type, :packaged_element)
225
+ all_elements.delete_if { |e| !e.is_a?(::Xmi::Uml::PackagedElement) }
226
+ end
227
+
228
+ private
229
+
230
+ # --- Model setup ---
231
+
232
+ def setup_model(xmi_model)
233
+ @xmi_root_model ||= xmi_model
234
+ if @xmi_index.nil?
235
+ @xmi_index = @xmi_root_model.index
236
+ @id_name_mapping = @xmi_index.id_name_map
237
+ end
238
+ end
239
+
240
+ # --- Document building ---
241
+
242
+ def build_document(xmi_model)
243
+ ::Lutaml::Uml::Document.new.tap do |doc|
244
+ doc.name = xmi_model.model.name
245
+ doc.packages = build_packages(xmi_model.model)
246
+ aggregate_document_extensions(doc)
247
+ end
248
+ end
249
+
250
+ # Walk every package in the tree and aggregate document-level
251
+ # collections from per-package state. This brings the XMI
252
+ # parser output to parity with the QEA parser, which populates
253
+ # `document.diagrams` and `document.associations` directly
254
+ # during transformation.
255
+ def aggregate_document_extensions(doc)
256
+ diagrams = []
257
+ class_associations = []
258
+ walk_packages(doc.packages) do |pkg|
259
+ diagrams.concat(pkg.diagrams || [])
260
+ pkg.classes&.each { |k| class_associations.concat(k.associations || []) }
261
+ end
262
+ package_associations = build_package_level_associations
263
+ doc.diagrams = diagrams
264
+ doc.associations = class_associations + package_associations
265
+ end
266
+
267
+ def walk_packages(packages, &block)
268
+ return unless packages
269
+
270
+ packages.each do |pkg|
271
+ yield pkg
272
+ walk_packages(pkg.packages, &block)
273
+ end
274
+ end
275
+
276
+ # Walks the raw XMI tree and collects every `<packagedElement
277
+ # xmi:type="uml:Association">` at any depth. Each becomes a
278
+ # `Lutaml::Uml::Association` with owner_end / member_end resolved
279
+ # from the `<memberEnd idref="...">` children. These are
280
+ # document-level relationships — UML associations live at the
281
+ # package level, not inside the participating classes.
282
+ def build_package_level_associations
283
+ result = []
284
+ walk_xmi_packaged_elements(@xmi_root_model.model) do |element|
285
+ next unless element.type?("uml:Association")
286
+
287
+ assoc = build_package_association(element)
288
+ result << assoc if assoc
289
+ end
290
+ result
291
+ end
292
+
293
+ def build_package_association(element)
294
+ member_ends = element.member_ends || []
295
+ return nil if member_ends.empty?
296
+
297
+ owner_ref = member_ends.first&.idref
298
+ member_ref = member_ends.last&.idref
299
+
300
+ ::Lutaml::Uml::Association.new.tap do |a|
301
+ a.xmi_id = element.id
302
+ a.name = element.name
303
+ a.owner_end = lookup_entity_name(owner_ref) || owner_ref
304
+ a.member_end = lookup_entity_name(member_ref) || member_ref
305
+ a.owner_end_xmi_id = owner_ref
306
+ a.member_end_xmi_id = member_ref
307
+ end
308
+ end
309
+
310
+ def walk_xmi_packaged_elements(node, &block)
311
+ return unless node.is_a?(::Xmi::Uml::PackagedElement) ||
312
+ node.is_a?(::Xmi::Uml::UmlModel)
313
+ return unless node.packaged_element
314
+
315
+ node.packaged_element.each do |element|
316
+ yield element
317
+ walk_xmi_packaged_elements(element, &block)
318
+ end
319
+ end
320
+
321
+ def build_packages(model)
322
+ return [] if model.packaged_element.nil?
323
+
324
+ packages = model.packaged_element.select { |e| e.type?("uml:Package") }
325
+ packages.map { |package| build_package(package) }
326
+ end
327
+
328
+ def build_package(package)
329
+ ::Lutaml::Uml::Package.new.tap do |pkg|
330
+ pkg.xmi_id = package.id
331
+ pkg.name = get_package_name(package)
332
+ pkg.definition = doc_node_attribute_value(package.id, "documentation")
333
+ st = doc_node_attribute_value(package.id, "stereotype")
334
+ pkg.stereotype = [st] if st
335
+
336
+ pkg.packages = build_packages(package)
337
+ pkg.classes = build_classes(package)
338
+ pkg.enums = build_enums(package)
339
+ pkg.data_types = build_data_types(package)
340
+ pkg.instances = build_instances(package)
341
+ pkg.diagrams = build_diagrams(package.id)
342
+ end
343
+ end
344
+
345
+ def build_classes(package)
346
+ return [] if package.packaged_element.nil?
347
+
348
+ klasses = package.packaged_element.select do |e|
349
+ e.type?("uml:Class") || e.type?("uml:AssociationClass") ||
350
+ e.type?("uml:Interface") || e.type?("uml:Signal") ||
351
+ e.type?("uml:Component")
352
+ end
353
+
354
+ klasses.map { |klass| build_class(klass) }
355
+ end
356
+
357
+ # Walks `<packagedElement xmi:type="uml:InstanceSpecification">`
358
+ # children of a package. Each becomes a `Lutaml::Uml::Instance`
359
+ # with name, xmi_id, classifier (resolved via id_name_mapping),
360
+ # and definition. Mirrors the QEA factory's InstanceTransformer.
361
+ def build_instances(package)
362
+ return [] if package.packaged_element.nil?
363
+
364
+ package.packaged_element
365
+ .select { |e| e.type?("uml:InstanceSpecification") }
366
+ .map { |spec| build_instance(spec) }
367
+ end
368
+
369
+ def build_instance(spec)
370
+ ::Lutaml::Uml::Instance.new.tap do |instance|
371
+ instance.xmi_id = spec.id
372
+ instance.name = spec.name
373
+ instance.definition = doc_node_attribute_value(spec.id, "documentation")
374
+ classifier_ref = spec.classifier
375
+ instance.classifier = lookup_entity_name(classifier_ref) if classifier_ref
376
+ end
377
+ end
378
+
379
+ def build_class(klass) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics:CyclomaticComplexity,Metrics:PerceivedComplexity
380
+ ::Lutaml::Uml::UmlClass.new.tap do |k|
381
+ k.xmi_id = klass.id
382
+ k.name = klass.name
383
+ k.type = klass.type.split(":").last
384
+ k.is_abstract = doc_node_attribute_value(klass.id, "isAbstract")
385
+ k.definition = doc_node_attribute_value(klass.id, "documentation")
386
+ k_st = doc_node_attribute_value(klass.id, "stereotype")
387
+ k.stereotype = [k_st] if k_st
388
+
389
+ k.attributes = build_class_attributes(klass)
390
+ k.associations = build_associations(klass.id)
391
+ k.operations = build_operations(klass)
392
+ k.constraints = build_constraints(klass.id)
393
+ k.association_generalization = build_assoc_generalizations(klass)
394
+
395
+ if klass.type?("uml:Class")
396
+ k.generalization = build_generalization(klass)
397
+ end
398
+ end
399
+ end
400
+
401
+ def build_enums(package) # rubocop:disable Metrics:MethodLength,Metrics:AbcSize
402
+ return [] if package.packaged_element.nil?
403
+
404
+ package.packaged_element
405
+ .select { |e| e.type?("uml:Enumeration") }
406
+ .map do |enum|
407
+ ::Lutaml::Uml::Enum.new.tap do |en|
408
+ en.xmi_id = enum.id
409
+ en.name = enum.name
410
+ en.values = build_values(enum)
411
+ en.definition = doc_node_attribute_value(enum.id, "documentation")
412
+ en_st = doc_node_attribute_value(enum.id, "stereotype")
413
+ en.stereotype = [en_st] if en_st
414
+ end
415
+ end
416
+ end
417
+
418
+ def build_data_types(package) # rubocop:disable Metrics:AbcSize,Metrics:MethodLength
419
+ return [] if package.packaged_element.nil?
420
+
421
+ package.packaged_element
422
+ .select { |e| e.type?("uml:DataType") }
423
+ .map do |dt|
424
+ ::Lutaml::Uml::DataType.new.tap do |data_type|
425
+ data_type.xmi_id = dt.id
426
+ data_type.name = dt.name
427
+ data_type.is_abstract = doc_node_attribute_value(dt.id, "isAbstract")
428
+ data_type.definition = doc_node_attribute_value(dt.id, "documentation")
429
+ dt_st = doc_node_attribute_value(dt.id, "stereotype")
430
+ data_type.stereotype = [dt_st] if dt_st
431
+
432
+ data_type.attributes = build_class_attributes(dt)
433
+ data_type.operations = build_operations(dt)
434
+ data_type.associations = build_associations(dt.id)
435
+ data_type.constraints = build_constraints(dt.id)
436
+ end
437
+ end
438
+ end
439
+
440
+ def build_diagrams(node_id) # rubocop:disable Metrics:AbcSize,Metrics:MethodLength
441
+ return [] if @xmi_root_model.extension&.diagrams&.diagram.nil?
442
+
443
+ diagram_lookup[node_id].map do |diagram|
444
+ ::Lutaml::Uml::Diagram.new.tap do |dia|
445
+ dia.xmi_id = diagram.id
446
+ dia.name = diagram&.properties&.name
447
+ dia.definition = diagram&.properties&.documentation
448
+
449
+ package_id = diagram&.model&.package
450
+ if package_id
451
+ dia.package_id = package_id
452
+ dia.package_name = find_packaged_element_by_id(package_id)&.name
453
+ end
454
+ end
455
+ end
456
+ end
457
+
458
+ def build_class_attributes(klass) # rubocop:disable Metrics:AbcSize,Metrics:CyclomaticComplexity,Metrics:MethodLength
459
+ return [] if klass.owned_attribute.nil?
460
+
461
+ all_props = klass.owned_attribute.select { |attr| attr.type?("uml:Property") }
462
+ all_props.filter_map { |oa| build_attribute(oa) }
463
+ end
464
+
465
+ def build_attribute(owned_attr) # rubocop:disable Metrics:AbcSize,Metrics:MethodLength
466
+ uml_type = owned_attr.uml_type
467
+ uml_type_idref = uml_type.idref if uml_type
468
+
469
+ ::Lutaml::Uml::TopElementAttribute.new.tap do |attr|
470
+ attr.id = owned_attr.id
471
+ attr.name = owned_attr.name
472
+ attr.type = lookup_entity_name(uml_type_idref) || uml_type_idref
473
+ attr.xmi_id = uml_type_idref
474
+ attr.is_derived = !!owned_attr.is_derived
475
+ attr.cardinality = ::Lutaml::Uml::Cardinality.new.tap do |car|
476
+ car.min = owned_attr.lower_value&.value
477
+ car.max = owned_attr.upper_value&.value
478
+ end
479
+ attr.definition = lookup_attribute_documentation(owned_attr.id)
480
+
481
+ if owned_attr.association
482
+ attr.association = owned_attr.association
483
+ attr.definition = lookup_assoc_def(owned_attr.association)
484
+ attr.type_ns = get_ns_by_xmi_id(attr.xmi_id)
485
+ end
486
+ end
487
+ end
488
+
489
+ def build_associations(xmi_id) # rubocop:disable Metrics:AbcSize,Metrics:CyclomaticComplexity,Metrics:MethodLength,Metrics:PerceivedComplexity
490
+ matched_element = xmi_index&.find_element(xmi_id)
491
+ return if !matched_element || !matched_element.links
492
+
493
+ links = []
494
+ matched_element.links.each do |link|
495
+ links << link.association if link.association.any?
496
+ end
497
+
498
+ links.flatten.compact.filter_map do |assoc|
499
+ build_association(assoc, xmi_id)
500
+ end
501
+ end
502
+
503
+ def build_association(assoc, xmi_id) # rubocop:disable Metrics:AbcSize,Metrics:CyclomaticComplexity,Metrics:MethodLength
504
+ link_member = assoc.start == xmi_id ? "end" : "start"
505
+ link_owner = link_member == "start" ? "end" : "start"
506
+
507
+ member_end, member_end_type, member_end_cardinality,
508
+ member_end_attribute_name, member_end_xmi_id =
509
+ serialize_member_type(xmi_id, assoc, link_member)
510
+
511
+ owner_end = serialize_owned_type(xmi_id, assoc, link_owner)
512
+ doc_node = link_member == "start" ? "source" : "target"
513
+ definition = fetch_definition_node_value(assoc.id, doc_node)
514
+
515
+ owner_end_attribute_name = find_owner_attribute_name(xmi_id, assoc.id)
516
+
517
+ return nil unless member_end &&
518
+ ((member_end_type != "aggregation") ||
519
+ (member_end_type == "aggregation" && member_end_attribute_name))
520
+
521
+ ::Lutaml::Uml::Association.new.tap do |association|
522
+ association.xmi_id = assoc.id
523
+ association.member_end = member_end
524
+ association.member_end_type = member_end_type
525
+ association.member_end_cardinality = build_cardinality(member_end_cardinality)
526
+ association.member_end_attribute_name = member_end_attribute_name
527
+ association.member_end_xmi_id = member_end_xmi_id
528
+ association.owner_end = owner_end
529
+ association.owner_end_xmi_id = xmi_id
530
+ association.owner_end_attribute_name = owner_end_attribute_name
531
+ association.definition = definition
532
+ end
533
+ end
534
+
535
+ def build_cardinality(hash)
536
+ return nil unless hash
537
+
538
+ ::Lutaml::Uml::Cardinality.new.tap do |cardinality|
539
+ cardinality.min = hash[:min]
540
+ cardinality.max = hash[:max]
541
+ end
542
+ end
543
+
544
+ def build_operations(klass) # rubocop:disable Metrics:MethodLength,Metrics:AbcSize
545
+ return [] if klass.owned_operation.nil?
546
+
547
+ klass.owned_operation.filter_map do |operation|
548
+ uml_type = operation.uml_type.first
549
+ uml_type_idref = uml_type.idref if uml_type
550
+
551
+ if !operation.class.attributes.key?(:association) || operation.association.nil?
552
+ ::Lutaml::Uml::Operation.new.tap do |op|
553
+ op.id = operation.id
554
+ op.xmi_id = uml_type_idref
555
+ op.name = operation.name
556
+ op.definition = lookup_attribute_documentation(operation.id)
557
+ end
558
+ end
559
+ end
560
+ end
561
+
562
+ def build_constraints(klass_id) # rubocop:disable Metrics:MethodLength,Metrics:AbcSize
563
+ connector_node = fetch_connector(klass_id)
564
+ return [] if connector_node.nil?
565
+
566
+ constraints = %i[source target].map do |st|
567
+ connector_node.public_send(st).constraints.constraint
568
+ end.flatten
569
+
570
+ constraints.map do |constraint|
571
+ ::Lutaml::Uml::Constraint.new.tap do |con|
572
+ con.name = CGI.unescapeHTML(constraint.name)
573
+ con.type = constraint.type
574
+ con.weight = constraint.weight
575
+ con.status = constraint.status
576
+ end
577
+ end
578
+ end
579
+
580
+ def build_values(enum) # rubocop:disable Metrics:MethodLength,Metrics:CyclomaticComplexity,Metrics:AbcSize
581
+ return [] if enum.owned_literal.nil?
582
+
583
+ enum.owned_literal
584
+ .select { |lit| lit.type?("uml:EnumerationLiteral") }
585
+ .map do |owned_literal|
586
+ uml_type_id = owned_literal&.uml_type&.idref
587
+
588
+ ::Lutaml::Uml::Value.new.tap do |value|
589
+ value.name = owned_literal.name
590
+ value.type = lookup_entity_name(uml_type_id) || uml_type_id
591
+ value.definition = lookup_attribute_documentation(owned_literal.id)
592
+ end
593
+ end
594
+ end
595
+
596
+ # --- Generalization building ---
597
+
598
+ def build_generalization(klass) # rubocop:disable Metrics:AbcSize,Metrics:MethodLength,Metrics:CyclomaticComplexity,Metrics:PerceivedComplexity
599
+ uml_general_obj, next_general_node_id = get_uml_general(klass.id)
600
+ return uml_general_obj unless next_general_node_id
601
+
602
+ if uml_general_obj.general
603
+ inherited_props = []
604
+ inherited_assoc_props = []
605
+ level = 0
606
+
607
+ loop_general_item(
608
+ uml_general_obj.general, level, inherited_props, inherited_assoc_props
609
+ )
610
+ uml_general_obj.inherited_props = inherited_props.reverse
611
+ uml_general_obj.inherited_assoc_props = inherited_assoc_props.reverse
612
+ end
613
+
614
+ uml_general_obj
615
+ end
616
+
617
+ def build_generalization_attributes(uml_general_obj) # rubocop:disable Metrics:AbcSize,Metrics:MethodLength
618
+ upper_klass = uml_general_obj.general_upper_klass
619
+ gen_attrs = uml_general_obj.general_attributes
620
+ gen_name = uml_general_obj.general_name
621
+
622
+ gen_attrs&.each do |i|
623
+ name_ns = case i.type_ns
624
+ when "core", "gml"
625
+ upper_klass
626
+ else
627
+ i.type_ns
628
+ end
629
+ name_ns = upper_klass if name_ns.nil?
630
+
631
+ i.name_ns = name_ns
632
+ i.gen_name = gen_name
633
+ i.name = "" if i.name.nil?
634
+ end
635
+
636
+ gen_attrs
637
+ end
638
+
639
+ def get_uml_general(general_id) # rubocop:disable Metrics:AbcSize,Metrics:MethodLength
640
+ general_node = find_packaged_element_by_id(general_id)
641
+ return [] unless general_node
642
+
643
+ general_node_attrs = get_uml_general_attributes(general_node)
644
+ general_upper_klass = find_upper_level_packaged_element(general_id)
645
+ next_general_node_id = general_node.generalization.first&.general
646
+
647
+ uml_general = build_uml_general_node(
648
+ general_id, general_node, general_node_attrs,
649
+ general_upper_klass, next_general_node_id
650
+ )
651
+
652
+ assign_general_properties(uml_general)
653
+
654
+ [uml_general, next_general_node_id]
655
+ end
656
+
657
+ def build_uml_general_node(general_id, general_node, attrs, upper_klass, next_id)
658
+ gen = ::Lutaml::Uml::Generalization.new
659
+ assign_general_basic_props(gen, general_id, general_node, attrs, upper_klass)
660
+ assign_stereotype(gen, general_id)
661
+ assign_parent_generalization(gen, general_node, next_id)
662
+ gen
663
+ end
664
+
665
+ def assign_general_basic_props(gen, general_id, general_node, attrs, upper_klass)
666
+ gen.general_id = general_id
667
+ gen.general_name = general_node.name
668
+ gen.general_attributes = attrs
669
+ gen.general_upper_klass = upper_klass ? get_package_name(upper_klass) : nil
670
+ gen.name = general_node.name
671
+ gen.type = general_node.type
672
+ gen.definition = lookup_element_prop_documentation(general_id)
673
+ end
674
+
675
+ def assign_stereotype(gen, general_id)
676
+ gen_st = doc_node_attribute_value(general_id, "stereotype")
677
+ gen.stereotype = [gen_st] if gen_st
678
+ end
679
+
680
+ def assign_parent_generalization(gen, general_node, next_id)
681
+ return unless next_id
682
+
683
+ gen.general = set_uml_generalization(next_id)
684
+ gen.has_general = true
685
+ gen.general_id = general_node.id
686
+ gen.general_name = general_node.name
687
+ end
688
+
689
+ def assign_general_properties(uml_general)
690
+ uml_general.attributes = build_generalization_attributes(uml_general)
691
+ uml_general.owned_props = uml_general.attributes.select { |a| a.association.nil? }
692
+ uml_general.assoc_props = uml_general.attributes.select(&:association)
693
+ end
694
+
695
+ def get_uml_general_attributes(general_node) # rubocop:disable Metrics:AbcSize,Metrics:MethodLength
696
+ attrs = build_class_attributes(general_node)
697
+
698
+ attrs.map do |attr|
699
+ ::Lutaml::Uml::GeneralAttribute.new.tap do |gen_attr|
700
+ gen_attr.id = attr.id
701
+ gen_attr.name = attr.name
702
+ gen_attr.type = attr.type
703
+ gen_attr.xmi_id = attr.xmi_id
704
+ gen_attr.is_derived = !!attr.is_derived
705
+ gen_attr.cardinality = attr.cardinality
706
+ gen_attr.definition = attr.definition&.strip
707
+ gen_attr.association = attr.association
708
+ gen_attr.has_association = !!attr.association
709
+ gen_attr.type_ns = attr.type_ns
710
+ end
711
+ end
712
+ end
713
+
714
+ def set_uml_generalization(general_id)
715
+ uml_general_obj, next_general_node_id = get_uml_general(general_id)
716
+
717
+ if next_general_node_id
718
+ uml_general_obj.general = set_uml_generalization(next_general_node_id)
719
+ uml_general_obj.has_general = true
720
+ end
721
+
722
+ uml_general_obj
723
+ end
724
+
725
+ def loop_general_item( # rubocop:disable Metrics:MethodLength,Metrics:AbcSize,Metrics:PerceivedComplexity,Metrics:CyclomaticComplexity
726
+ general_item, level, inherited_props, inherited_assoc_props
727
+ )
728
+ gen_upper_klass = general_item.general_upper_klass
729
+ gen_name = general_item.general_name
730
+
731
+ general_item.attributes.reverse_each do |attr|
732
+ attr.upper_klass = gen_upper_klass
733
+ attr.gen_name = gen_name
734
+ attr.level = level
735
+
736
+ if attr.association
737
+ inherited_assoc_props << attr
738
+ else
739
+ inherited_props << attr
740
+ end
741
+ end
742
+
743
+ if general_item&.has_general && general_item.general
744
+ level += 1
745
+ loop_general_item(
746
+ general_item.general, level, inherited_props, inherited_assoc_props
747
+ )
748
+ end
749
+ end
750
+
751
+ def build_assoc_generalizations(klass) # rubocop:disable Metrics:AbcSize
752
+ return [] if klass.generalization.nil? || klass.generalization.empty?
753
+
754
+ klass.generalization.map do |gen|
755
+ ::Lutaml::Uml::AssociationGeneralization.new.tap do |assoc_gen|
756
+ assoc_gen.id = gen.id
757
+ assoc_gen.type = gen.type
758
+ assoc_gen.general = gen.general
759
+ end
760
+ end
761
+ end
762
+
763
+ # --- Connector serialization ---
764
+
765
+ def serialize_owned_type(owner_xmi_id, link, link_owner_name)
766
+ case link
767
+ when ::Xmi::Sparx::Element::NoteLink
768
+ return
769
+ when ::Xmi::Sparx::Element::Generalization
770
+ owner_end, = generalization_association(owner_xmi_id, link)
771
+ return owner_end
772
+ end
773
+
774
+ xmi_id = link.public_send(link_owner_name.to_sym)
775
+ lookup_entity_name(xmi_id) || connector_source_name(xmi_id)
776
+ end
777
+
778
+ def serialize_member_end(owner_xmi_id, link) # rubocop:disable Metrics:MethodLength,Metrics:AbcSize,Metrics:CyclomaticComplexity
779
+ case link.name
780
+ when "NoteLink"
781
+ return
782
+ when "Generalization"
783
+ return generalization_association(owner_xmi_id, link)
784
+ end
785
+
786
+ xmi_id = link.start
787
+ source_or_target = :source
788
+
789
+ if link.start == owner_xmi_id
790
+ xmi_id = link.end
791
+ source_or_target = :target
792
+ end
793
+
794
+ connector = fetch_connector(link.id)
795
+ ea_type = connector&.properties&.ea_type
796
+ member_end_type = ea_type&.downcase
797
+
798
+ member_end = member_end_name(xmi_id, source_or_target, link)
799
+ [member_end, member_end_type, xmi_id]
800
+ end
801
+
802
+ def member_end_name(xmi_id, source_or_target, link) # rubocop:disable Metrics:MethodLength
803
+ connector_label = connector_labels(xmi_id, source_or_target)
804
+ entity_name = lookup_entity_name(xmi_id)
805
+ connector_name = connector_name_by_source_or_target(xmi_id, source_or_target)
806
+
807
+ case link
808
+ when ::Xmi::Sparx::Element::Aggregation
809
+ connector_label || entity_name || connector_name
810
+ else
811
+ entity_name || connector_name
812
+ end
813
+ end
814
+
815
+ def serialize_member_type(owner_xmi_id, link, link_member_name) # rubocop:disable Metrics:MethodLength,Metrics:AbcSize
816
+ member_end, member_end_type, xmi_id =
817
+ serialize_member_end(owner_xmi_id, link)
818
+
819
+ if link.is_a?(::Xmi::Sparx::Element::Association)
820
+ connector_type = link_member_name == "start" ? "source" : "target"
821
+ member_end_cardinality, member_end_attribute_name =
822
+ fetch_assoc_connector(link.id, connector_type)
823
+ else
824
+ member_end_cardinality, member_end_attribute_name =
825
+ fetch_owned_attribute_node(xmi_id)
826
+ end
827
+
828
+ if fetch_connector_name(link.id)
829
+ member_end = fetch_connector_name(link.id)
830
+ end
831
+
832
+ [member_end, member_end_type, member_end_cardinality,
833
+ member_end_attribute_name, xmi_id]
834
+ end
835
+
836
+ def fetch_connector_name(link_id)
837
+ connector = fetch_connector(link_id)
838
+ connector&.name
839
+ end
840
+
841
+ def fetch_assoc_connector(link_id, connector_type)
842
+ connector = fetch_connector(link_id)
843
+ return [nil, nil] unless connector
844
+
845
+ assoc_connector = connector.public_send(connector_type.to_sym)
846
+ return [nil, nil] unless assoc_connector
847
+
848
+ [extract_cardinality(assoc_connector), extract_attribute_name(assoc_connector)]
849
+ end
850
+
851
+ def extract_cardinality(assoc_connector)
852
+ assoc_connector_type = assoc_connector.type
853
+ min = nil
854
+ max = nil
855
+ if assoc_connector_type&.multiplicity
856
+ cardinality = assoc_connector_type.multiplicity.split("..")
857
+ cardinality.unshift("1") if cardinality.length == 1
858
+ min, max = cardinality
859
+ end
860
+ cardinality_min_max_value(min, max)
861
+ end
862
+
863
+ def extract_attribute_name(assoc_connector)
864
+ assoc_connector.role ? assoc_connector.model.name : nil
865
+ end
866
+
867
+ def generalization_association(owner_xmi_id, link) # rubocop:disable Metrics:MethodLength
868
+ member_end_type = "generalization"
869
+ xmi_id = link.start
870
+ source_or_target = :source
871
+
872
+ if link.start == owner_xmi_id
873
+ member_end_type = "inheritance"
874
+ xmi_id = link.end
875
+ source_or_target = :target
876
+ end
877
+
878
+ member_end = member_end_name(xmi_id, source_or_target, link)
879
+ [member_end, member_end_type, xmi_id]
880
+ end
881
+
882
+ def fetch_owned_attribute_node(xmi_id)
883
+ oa = xmi_index.find_owned_attrs_by_type(xmi_id)
884
+ .find { |a| !!a.association }
885
+
886
+ if oa
887
+ cardinality = cardinality_min_max_value(
888
+ oa.lower_value&.value, oa.upper_value&.value
889
+ )
890
+ oa_name = oa.name
891
+ end
892
+
893
+ [cardinality, oa_name]
894
+ end
895
+
896
+ def find_owner_attribute_name(owner_xmi_id, assoc_id)
897
+ owner_node = find_packaged_element_by_id(owner_xmi_id)
898
+ return nil unless owner_node&.owned_attribute
899
+
900
+ owned_attr = owner_node.owned_attribute.find { |oa| oa.association == assoc_id }
901
+ owned_attr&.name
902
+ end
903
+
904
+ # --- Connector lookup index ---
905
+
906
+ def diagram_lookup
907
+ @diagram_lookup ||= begin
908
+ idx = Hash.new { |h, k| h[k] = [] }
909
+ xmi_diagrams.each { |d| idx[d.model.package] << d if d.model&.package }
910
+ idx
911
+ end
912
+ end
913
+
914
+ def xmi_diagrams
915
+ @xmi_root_model.extension&.diagrams&.diagram || []
916
+ end
917
+
918
+ def connector_lookup
919
+ @connector_lookup ||= begin
920
+ lookup = {}
921
+ connectors = @xmi_root_model.extension&.connectors&.connector || []
922
+ connectors.each { |con| index_connector_directions(con, lookup) }
923
+ lookup
924
+ end
925
+ end
926
+
927
+ def index_connector_directions(con, lookup)
928
+ %i[source target].each do |dir|
929
+ idref = con.public_send(dir)&.idref
930
+ lookup[[dir, idref]] = con if idref
931
+ end
932
+ end
933
+
934
+ def connector_node_by_id(xmi_id, source_or_target)
935
+ connector_lookup[[source_or_target.to_sym, xmi_id]]
936
+ end
937
+
938
+ def connector_name_by_source_or_target(xmi_id, source_or_target) # rubocop:disable Metrics:AbcSize
939
+ node = connector_node_by_id(xmi_id, source_or_target)
940
+ return node.name if node&.name
941
+
942
+ return if node.nil? ||
943
+ node.public_send(source_or_target.to_sym).nil? ||
944
+ node.public_send(source_or_target.to_sym).model.nil?
945
+
946
+ node.public_send(source_or_target.to_sym).model.name
947
+ end
948
+
949
+ def connector_labels(xmi_id, source_or_target)
950
+ node = connector_node_by_id(xmi_id, source_or_target)
951
+ return if node.nil?
952
+
953
+ node.labels&.rt || node.labels&.lt
954
+ end
955
+
956
+ def connector_source_name(xmi_id)
957
+ connector_name_by_source_or_target(xmi_id, :source)
958
+ end
959
+
960
+ def cardinality_min_max_value(min, max)
961
+ { min: min, max: max }
962
+ end
963
+
964
+ def select_all_items(items, model, type, method)
965
+ iterate_tree(items, model, type, method.to_sym)
966
+ end
967
+
968
+ def iterate_tree(result, node, type, children_method) # rubocop:disable Metrics:AbcSize,Metrics:CyclomaticComplexity,Metrics:PerceivedComplexity
969
+ result << node if type.nil? || node.type == type
970
+ return unless node.public_send(children_method)
971
+
972
+ node.public_send(children_method).each do |sub_node|
973
+ if sub_node.public_send(children_method)
974
+ iterate_tree(result, sub_node, type, children_method)
975
+ elsif type.nil? || sub_node.type == type
976
+ result << sub_node
977
+ end
978
+ end
979
+ end
980
+
981
+ def find_klass_packaged_element_by_path(path)
982
+ if path.absolute?
983
+ iterate_packaged_element(@xmi_root_model.model, path.segments.map(&:name))
984
+ else
985
+ iterate_relative_packaged_element(path.segments.map(&:name))
986
+ end
987
+ end
988
+
989
+ def iterate_relative_packaged_element(name_array)
990
+ matched_elements = xmi_index.packaged_elements_of_type("uml:Package")
991
+ .select { |e| e.name == name_array[0] }
992
+
993
+ result = matched_elements.map do |e|
994
+ iterate_packaged_element(e, name_array, type: "uml:Class")
995
+ end
996
+
997
+ result.compact.first
998
+ end
999
+
1000
+ def iterate_packaged_element(model, name_array, index: 1, type: "uml:Package")
1001
+ return model if index == name_array.count
1002
+
1003
+ model = model.packaged_element.find do |p|
1004
+ p.name == name_array[index] && p.type?(type)
1005
+ end
1006
+
1007
+ return nil if model.nil?
1008
+
1009
+ index += 1
1010
+ type = index == name_array.count - 1 ? "uml:Class" : "uml:Package"
1011
+ iterate_packaged_element(model, name_array, index: index, type: type)
1012
+ end
1013
+ end
1014
+ end
1015
+ end