ea 0.1.0 → 0.1.4

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 (227) 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/config/diagram_styles.yml +200 -0
  43. data/config/model_transformations.yml +266 -0
  44. data/config/qea_schema.yml +1024 -0
  45. data/docs/ea_to_uml_type_mapping.md +89 -0
  46. data/docs/xmi_qea_conversion_capabilities.md +99 -0
  47. data/examples/lur/20251010_current_plateau_v5.1.lur +0 -0
  48. data/examples/lur/basic.lur +0 -0
  49. data/examples/lur/test-output.lur +0 -0
  50. data/examples/lur/test.lur +0 -0
  51. data/examples/lur_basic_usage.rb +221 -0
  52. data/examples/lur_cli_workflow.rb +263 -0
  53. data/examples/lur_statistics.rb +326 -0
  54. data/examples/qea/20251010_current_plateau_v5.1.qea +0 -0
  55. data/examples/qea/ArcGISWorkspace_template.qea +0 -0
  56. data/examples/qea/README_qea_parser.adoc +230 -0
  57. data/examples/qea/UmlModel_template.qea +0 -0
  58. data/examples/qea/basic.qea +0 -0
  59. data/examples/qea/simple.qea +0 -0
  60. data/examples/qea/simple_example.qea +0 -0
  61. data/examples/qea/test.qea +0 -0
  62. data/examples/qea_standalone_query.rb +73 -0
  63. data/examples/qea_to_repository.rb +51 -0
  64. data/examples/smoke_test_real_qea.rb +81 -0
  65. data/exe/ea +7 -0
  66. data/lib/ea/cli/app.rb +72 -0
  67. data/lib/ea/cli/command/base.rb +80 -0
  68. data/lib/ea/cli/command/convert.rb +62 -0
  69. data/lib/ea/cli/command/diagrams.rb +81 -0
  70. data/lib/ea/cli/command/list.rb +61 -0
  71. data/lib/ea/cli/command/parse.rb +29 -0
  72. data/lib/ea/cli/command/stats.rb +20 -0
  73. data/lib/ea/cli/command/validate.rb +41 -0
  74. data/lib/ea/cli/command.rb +15 -0
  75. data/lib/ea/cli/error.rb +34 -0
  76. data/lib/ea/cli/output/formatter.rb +34 -0
  77. data/lib/ea/cli/output/json_formatter.rb +20 -0
  78. data/lib/ea/cli/output/table_formatter.rb +42 -0
  79. data/lib/ea/cli/output/yaml_formatter.rb +20 -0
  80. data/lib/ea/cli/output.rb +56 -0
  81. data/lib/ea/cli.rb +17 -0
  82. data/lib/ea/diagram/configuration.rb +379 -0
  83. data/lib/ea/diagram/element_renderers/base_renderer.rb +77 -0
  84. data/lib/ea/diagram/element_renderers/class_renderer.rb +323 -0
  85. data/lib/ea/diagram/element_renderers/connector_renderer.rb +41 -0
  86. data/lib/ea/diagram/element_renderers/package_renderer.rb +61 -0
  87. data/lib/ea/diagram/element_renderers.rb +43 -0
  88. data/lib/ea/diagram/extractor.rb +560 -0
  89. data/lib/ea/diagram/layout_engine.rb +170 -0
  90. data/lib/ea/diagram/path_builder.rb +202 -0
  91. data/lib/ea/diagram/style_parser.rb +42 -0
  92. data/lib/ea/diagram/style_resolver.rb +276 -0
  93. data/lib/ea/diagram/svg_renderer.rb +274 -0
  94. data/lib/ea/diagram/util.rb +73 -0
  95. data/lib/ea/diagram.rb +47 -0
  96. data/lib/ea/qea/benchmark.rb +210 -0
  97. data/lib/ea/qea/database.rb +308 -0
  98. data/lib/ea/qea/factory/association_builder.rb +203 -0
  99. data/lib/ea/qea/factory/association_transformer.rb +91 -0
  100. data/lib/ea/qea/factory/attribute_tag_transformer.rb +57 -0
  101. data/lib/ea/qea/factory/attribute_transformer.rb +93 -0
  102. data/lib/ea/qea/factory/base_transformer.rb +177 -0
  103. data/lib/ea/qea/factory/class_transformer.rb +116 -0
  104. data/lib/ea/qea/factory/constraint_transformer.rb +75 -0
  105. data/lib/ea/qea/factory/data_type_transformer.rb +77 -0
  106. data/lib/ea/qea/factory/diagram_transformer.rb +157 -0
  107. data/lib/ea/qea/factory/document_builder.rb +283 -0
  108. data/lib/ea/qea/factory/ea_to_uml_factory.rb +229 -0
  109. data/lib/ea/qea/factory/enum_transformer.rb +74 -0
  110. data/lib/ea/qea/factory/generalization_builder.rb +227 -0
  111. data/lib/ea/qea/factory/generalization_transformer.rb +98 -0
  112. data/lib/ea/qea/factory/instance_transformer.rb +68 -0
  113. data/lib/ea/qea/factory/object_property_transformer.rb +58 -0
  114. data/lib/ea/qea/factory/operation_transformer.rb +66 -0
  115. data/lib/ea/qea/factory/package_transformer.rb +145 -0
  116. data/lib/ea/qea/factory/reference_resolver.rb +99 -0
  117. data/lib/ea/qea/factory/stereotype_loader.rb +39 -0
  118. data/lib/ea/qea/factory/tagged_value_transformer.rb +38 -0
  119. data/lib/ea/qea/factory/transformer_registry.rb +80 -0
  120. data/lib/ea/qea/factory.rb +37 -0
  121. data/lib/ea/qea/file_detector.rb +178 -0
  122. data/lib/ea/qea/infrastructure/database_connection.rb +100 -0
  123. data/lib/ea/qea/infrastructure/schema_reader.rb +136 -0
  124. data/lib/ea/qea/infrastructure/table_reader.rb +224 -0
  125. data/lib/ea/qea/infrastructure.rb +12 -0
  126. data/lib/ea/qea/models/base_model.rb +59 -0
  127. data/lib/ea/qea/models/ea_attribute.rb +109 -0
  128. data/lib/ea/qea/models/ea_attribute_tag.rb +100 -0
  129. data/lib/ea/qea/models/ea_complexity_type.rb +79 -0
  130. data/lib/ea/qea/models/ea_connector.rb +160 -0
  131. data/lib/ea/qea/models/ea_connector_type.rb +60 -0
  132. data/lib/ea/qea/models/ea_constraint_type.rb +63 -0
  133. data/lib/ea/qea/models/ea_datatype.rb +104 -0
  134. data/lib/ea/qea/models/ea_diagram.rb +115 -0
  135. data/lib/ea/qea/models/ea_diagram_link.rb +78 -0
  136. data/lib/ea/qea/models/ea_diagram_object.rb +73 -0
  137. data/lib/ea/qea/models/ea_diagram_type.rb +56 -0
  138. data/lib/ea/qea/models/ea_document.rb +63 -0
  139. data/lib/ea/qea/models/ea_object.rb +223 -0
  140. data/lib/ea/qea/models/ea_object_constraint.rb +53 -0
  141. data/lib/ea/qea/models/ea_object_property.rb +87 -0
  142. data/lib/ea/qea/models/ea_object_type.rb +73 -0
  143. data/lib/ea/qea/models/ea_operation.rb +127 -0
  144. data/lib/ea/qea/models/ea_operation_param.rb +76 -0
  145. data/lib/ea/qea/models/ea_package.rb +78 -0
  146. data/lib/ea/qea/models/ea_script.rb +62 -0
  147. data/lib/ea/qea/models/ea_status_type.rb +66 -0
  148. data/lib/ea/qea/models/ea_stereotype.rb +57 -0
  149. data/lib/ea/qea/models/ea_tagged_value.rb +99 -0
  150. data/lib/ea/qea/models/ea_xref.rb +165 -0
  151. data/lib/ea/qea/models.rb +35 -0
  152. data/lib/ea/qea/repositories/base_repository.rb +225 -0
  153. data/lib/ea/qea/repositories/object_repository.rb +219 -0
  154. data/lib/ea/qea/repositories.rb +10 -0
  155. data/lib/ea/qea/services/configuration.rb +211 -0
  156. data/lib/ea/qea/services/database_loader.rb +191 -0
  157. data/lib/ea/qea/services.rb +10 -0
  158. data/lib/ea/qea/validation/association_validator.rb +73 -0
  159. data/lib/ea/qea/validation/attribute_validator.rb +91 -0
  160. data/lib/ea/qea/validation/base_validator.rb +331 -0
  161. data/lib/ea/qea/validation/class_validator.rb +121 -0
  162. data/lib/ea/qea/validation/database/circular_reference_validator.rb +109 -0
  163. data/lib/ea/qea/validation/database/orphan_validator.rb +153 -0
  164. data/lib/ea/qea/validation/database/referential_integrity_validator.rb +128 -0
  165. data/lib/ea/qea/validation/database.rb +16 -0
  166. data/lib/ea/qea/validation/diagram_validator.rb +112 -0
  167. data/lib/ea/qea/validation/formatters/json_formatter.rb +137 -0
  168. data/lib/ea/qea/validation/formatters/text_formatter.rb +235 -0
  169. data/lib/ea/qea/validation/formatters.rb +12 -0
  170. data/lib/ea/qea/validation/operation_validator.rb +71 -0
  171. data/lib/ea/qea/validation/package_validator.rb +111 -0
  172. data/lib/ea/qea/validation/validation_engine.rb +387 -0
  173. data/lib/ea/qea/validation/validation_message.rb +144 -0
  174. data/lib/ea/qea/validation/validation_result.rb +210 -0
  175. data/lib/ea/qea/validation/validator_registry.rb +134 -0
  176. data/lib/ea/qea/validation.rb +28 -0
  177. data/lib/ea/qea/verification/comparison_result.rb +264 -0
  178. data/lib/ea/qea/verification/document_normalizer.rb +169 -0
  179. data/lib/ea/qea/verification/document_verifier.rb +322 -0
  180. data/lib/ea/qea/verification/element_comparator.rb +277 -0
  181. data/lib/ea/qea/verification/structure_matcher.rb +287 -0
  182. data/lib/ea/qea/verification.rb +14 -0
  183. data/lib/ea/qea.rb +185 -0
  184. data/lib/ea/transformations/configuration.rb +333 -0
  185. data/lib/ea/transformations/format_registry.rb +366 -0
  186. data/lib/ea/transformations/parsers/base_parser.rb +482 -0
  187. data/lib/ea/transformations/parsers/qea_parser.rb +401 -0
  188. data/lib/ea/transformations/parsers/xmi_parser.rb +243 -0
  189. data/lib/ea/transformations/transformation_engine.rb +390 -0
  190. data/lib/ea/transformations.rb +85 -0
  191. data/lib/ea/transformers/qea_to_xmi/association_end.rb +19 -0
  192. data/lib/ea/transformers/qea_to_xmi/cardinality.rb +96 -0
  193. data/lib/ea/transformers/qea_to_xmi/context.rb +106 -0
  194. data/lib/ea/transformers/qea_to_xmi/guid_format.rb +56 -0
  195. data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +92 -0
  196. data/lib/ea/transformers/qea_to_xmi/run_state.rb +107 -0
  197. data/lib/ea/transformers/qea_to_xmi/transformer.rb +607 -0
  198. data/lib/ea/transformers/qea_to_xmi/visibility.rb +73 -0
  199. data/lib/ea/transformers/qea_to_xmi.rb +29 -0
  200. data/lib/ea/transformers/uml_to_xmi/id_generator.rb +54 -0
  201. data/lib/ea/transformers/uml_to_xmi/transformer.rb +152 -0
  202. data/lib/ea/transformers/uml_to_xmi/writer.rb +96 -0
  203. data/lib/ea/transformers/uml_to_xmi.rb +16 -0
  204. data/lib/ea/transformers.rb +34 -0
  205. data/lib/ea/version.rb +1 -1
  206. data/lib/ea/xmi/liquid_drops/association_drop.rb +56 -0
  207. data/lib/ea/xmi/liquid_drops/attribute_drop.rb +72 -0
  208. data/lib/ea/xmi/liquid_drops/cardinality_drop.rb +35 -0
  209. data/lib/ea/xmi/liquid_drops/connector_drop.rb +54 -0
  210. data/lib/ea/xmi/liquid_drops/constraint_drop.rb +29 -0
  211. data/lib/ea/xmi/liquid_drops/data_type_drop.rb +63 -0
  212. data/lib/ea/xmi/liquid_drops/dependency_drop.rb +36 -0
  213. data/lib/ea/xmi/liquid_drops/diagram_drop.rb +34 -0
  214. data/lib/ea/xmi/liquid_drops/enum_drop.rb +49 -0
  215. data/lib/ea/xmi/liquid_drops/enum_owned_literal_drop.rb +25 -0
  216. data/lib/ea/xmi/liquid_drops/generalization_attribute_drop.rb +87 -0
  217. data/lib/ea/xmi/liquid_drops/generalization_drop.rb +127 -0
  218. data/lib/ea/xmi/liquid_drops/klass_drop.rb +191 -0
  219. data/lib/ea/xmi/liquid_drops/operation_drop.rb +29 -0
  220. data/lib/ea/xmi/liquid_drops/package_drop.rb +108 -0
  221. data/lib/ea/xmi/liquid_drops/root_drop.rb +34 -0
  222. data/lib/ea/xmi/liquid_drops/source_target_drop.rb +43 -0
  223. data/lib/ea/xmi/lookup_service.rb +89 -0
  224. data/lib/ea/xmi/parser.rb +919 -0
  225. data/lib/ea/xmi.rb +35 -0
  226. data/lib/ea.rb +10 -1
  227. metadata +382 -9
@@ -0,0 +1,607 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "xmi"
4
+
5
+ module Ea
6
+ module Transformers
7
+ module QeaToXmi
8
+ # Orchestrates serialization of an Ea::Qea::Database to Sparx XMI.
9
+ #
10
+ # Walks the package tree starting at root packages, constructing
11
+ # xmi gem models (Xmi::Uml::UmlModel, Xmi::Uml::PackagedElement,
12
+ # Xmi::Uml::OwnedAttribute, Xmi::Uml::OwnedEnd, etc.) from each
13
+ # QEA row, then asks the xmi gem to render them via
14
+ # `to_xml(use_prefix: true)` to produce Sparx XMI in the canonical
15
+ # mixed-prefix style. The serialized output is run through
16
+ # {XmlSanitizer} to strip truly-empty elements that the xmi gem's
17
+ # round-trip-oriented VALUE_MAP emits but Sparx XMI does not.
18
+ #
19
+ # Element-kind dispatch (Class vs Enumeration vs DataType vs
20
+ # Instance) is registry-driven — see CLASSIFIER_BUILDERS. Adding
21
+ # a new kind = adding one entry to that hash, no method change.
22
+ # Polymorphism for XMI element shape lives in the xmi gem's models
23
+ # (xmi:type discriminator on PackagedElement), not here.
24
+ #
25
+ # This is the FULL-FIDELITY path — no Lutaml::Uml::Document
26
+ # intermediate. Sparx-specific concepts (multiplicities, tagged
27
+ # values, stereotypes, primitive types, instance specifications,
28
+ # association ends) come straight from the QEA tables.
29
+ #
30
+ # Phase 2 will extend the xmi gem with visibility / isAbstract /
31
+ # classifier / aggregation attributes that the QEA database
32
+ # contains but the xmi gem's models don't yet declare.
33
+ class Transformer
34
+ MODEL_NAME = "EA_Model"
35
+ EXPORTER = "Enterprise Architect"
36
+ EXPORTER_VERSION = "6.5"
37
+
38
+ RELATIONSHIP_AT_PACKAGE_LEVEL = {
39
+ "Association" => :association,
40
+ "Aggregation" => :association,
41
+ "Composition" => :association,
42
+ "Dependency" => :dependency,
43
+ "Usage" => :dependency,
44
+ }.freeze
45
+
46
+ # OCP registry: maps EaObject#transformer_type to the builder
47
+ # that constructs the corresponding Xmi::Uml element. To add a
48
+ # new element kind (Signal, Interface, ...), append one entry
49
+ # here — `build_classifier` requires no edit.
50
+ #
51
+ # Builders are lambdas evaluated via `instance_exec`, so they
52
+ # run inside the Transformer instance and can call its private
53
+ # helpers without `send`/`public_send` dispatch.
54
+ CLASSIFIER_BUILDERS = {
55
+ class: ->(obj) { build_class(obj) },
56
+ enumeration: ->(obj) { build_enumeration(obj) },
57
+ data_type: ->(obj) { build_data_type(obj) },
58
+ instance: ->(obj) { build_instance(obj) },
59
+ }.freeze
60
+
61
+ def initialize(database)
62
+ @database = database
63
+ @context = Context.new(database: database)
64
+ end
65
+
66
+ # @return [String] XMI XML document
67
+ #
68
+ # The xmi gem's VALUE_MAP is generation-friendly
69
+ # (`to: { nil: :omitted, ... }`), so empty collections and
70
+ # nil-valued attributes are skipped at the source. No
71
+ # post-processing pass is needed — the prior XmlSanitizer
72
+ # workaround (TODO 21 §1) has been removed.
73
+ def serialize
74
+ build_root.to_xml(use_prefix: true)
75
+ end
76
+
77
+ private
78
+
79
+ # ---- Top-level construction --------------------------------------
80
+
81
+ def build_root
82
+ ::Xmi::Sparx::Root.new(
83
+ documentation: build_documentation,
84
+ model: build_model,
85
+ extension: build_extension,
86
+ )
87
+ end
88
+
89
+ def build_documentation
90
+ ::Xmi::Documentation.new(
91
+ exporter: EXPORTER,
92
+ exporter_version: EXPORTER_VERSION,
93
+ )
94
+ end
95
+
96
+ def build_model
97
+ ::Xmi::Uml::UmlModel.new(
98
+ type: "uml:Model",
99
+ name: MODEL_NAME,
100
+ packaged_element: root_packages.map { |pkg| build_package(pkg) },
101
+ )
102
+ end
103
+
104
+ def build_extension
105
+ ::Xmi::Sparx::Extension.new(extender: EXPORTER)
106
+ end
107
+
108
+ def root_packages
109
+ @database.packages
110
+ .select(&:root?)
111
+ .sort_by { |p| [p.tpos || 0, p.name.to_s] }
112
+ end
113
+
114
+ # ---- Package tree ------------------------------------------------
115
+
116
+ def build_package(pkg)
117
+ ::Xmi::Uml::PackagedElement.new(
118
+ type: "uml:Package",
119
+ id: @context.xmi_id_for(pkg, prefix: "EAPK"),
120
+ name: pkg.name,
121
+ owned_comment: package_comments(pkg),
122
+ packaged_element: package_children(pkg),
123
+ )
124
+ end
125
+
126
+ def package_children(pkg)
127
+ [
128
+ *subpackages(pkg),
129
+ *classifier_objects(pkg),
130
+ *package_level_relationships(pkg),
131
+ ].compact
132
+ end
133
+
134
+ def subpackages(pkg)
135
+ sorted_by_position(@context.child_packages(pkg.package_id))
136
+ .map { |sub| build_package(sub) }
137
+ end
138
+
139
+ def classifier_objects(pkg)
140
+ classifiers_in(pkg).map { |obj| build_classifier(obj) }.compact
141
+ end
142
+
143
+ # Connectors owned by this package: connectors whose start_object is
144
+ # a classifier in this package and whose type is a package-level
145
+ # relationship (Association / Dependency). Generalization and
146
+ # Realization are emitted inside the classifier itself.
147
+ def package_level_relationships(pkg)
148
+ emitted = Set.new
149
+ classifiers_in(pkg).flat_map do |obj|
150
+ @context.connectors_starting_at(obj.ea_object_id).filter_map do |conn|
151
+ key = RELATIONSHIP_AT_PACKAGE_LEVEL[conn.connector_type]
152
+ next nil unless key
153
+ next nil unless emitted.add?(conn.connector_id)
154
+
155
+ build_package_relationship(key, conn)
156
+ end
157
+ end
158
+ end
159
+
160
+ def build_package_relationship(kind, conn)
161
+ case kind
162
+ when :association then build_association(conn)
163
+ when :dependency then build_dependency(conn)
164
+ end
165
+ end
166
+
167
+ def package_comments(pkg)
168
+ notes_in(pkg).map { |obj| build_comment(obj) }
169
+ end
170
+
171
+ # ---- Classifier dispatch (OCP registry) --------------------------
172
+
173
+ def build_classifier(obj)
174
+ kind = obj.transformer_type || obj.object_type&.downcase&.to_sym
175
+ builder = CLASSIFIER_BUILDERS[kind]
176
+ return nil unless builder
177
+
178
+ instance_exec(obj, &builder)
179
+ end
180
+
181
+ def build_class(obj)
182
+ ::Xmi::Uml::PackagedElement.new(
183
+ type: class_xmi_type(obj),
184
+ id: @context.xmi_id_for(obj),
185
+ name: obj.name,
186
+ visibility: Visibility.from_scope(obj.scope),
187
+ is_abstract: Visibility.boolean_from_flag(obj.abstract),
188
+ generalization: generalizations_for(obj),
189
+ interface_realization: interface_realizations_for(obj),
190
+ owned_attribute: attributes_for(obj),
191
+ owned_operation: operations_for(obj),
192
+ )
193
+ end
194
+
195
+ def build_enumeration(obj)
196
+ ::Xmi::Uml::PackagedElement.new(
197
+ type: "uml:Enumeration",
198
+ id: @context.xmi_id_for(obj),
199
+ name: obj.name,
200
+ visibility: Visibility.from_scope(obj.scope),
201
+ owned_literal: enum_literals(obj),
202
+ )
203
+ end
204
+
205
+ def build_data_type(obj)
206
+ ::Xmi::Uml::PackagedElement.new(
207
+ type: primitive?(obj) ? "uml:PrimitiveType" : "uml:DataType",
208
+ id: @context.xmi_id_for(obj),
209
+ name: obj.name,
210
+ visibility: Visibility.from_scope(obj.scope),
211
+ owned_attribute: attributes_for(obj),
212
+ owned_operation: operations_for(obj),
213
+ )
214
+ end
215
+
216
+ def build_instance(obj)
217
+ ::Xmi::Uml::PackagedElement.new(
218
+ type: "uml:InstanceSpecification",
219
+ id: @context.xmi_id_for(obj),
220
+ name: obj.name,
221
+ visibility: Visibility.from_scope(obj.scope),
222
+ classifier: classifier_ref_for(obj),
223
+ slot: slots_for(obj),
224
+ )
225
+ end
226
+
227
+ def build_comment(obj)
228
+ ::Xmi::Uml::OwnedComment.new(
229
+ type: "uml:Comment",
230
+ id: @context.xmi_id_for(obj),
231
+ body_element: obj.note || obj.name || "",
232
+ )
233
+ end
234
+
235
+ # ---- Children of a classifier ------------------------------------
236
+
237
+ def generalizations_for(obj)
238
+ inheritance_connectors(obj, "Generalization")
239
+ .filter_map { |conn| build_generalization(conn) }
240
+ end
241
+
242
+ # Realization connectors point from a Class (client) to an
243
+ # Interface (supplier). Sparx's strict OMG output emits these
244
+ # as `<interfaceRealization>` children of the client class
245
+ # rather than as package-level `<packagedElement type=
246
+ # "uml:Realization">`. We do the same.
247
+ def interface_realizations_for(obj)
248
+ realization_connectors(obj).map { |conn| build_interface_realization(conn) }
249
+ end
250
+
251
+ def attributes_for(obj)
252
+ sorted_by_position(@context.attributes_for(obj.ea_object_id))
253
+ .map { |attr| build_attribute(attr) }
254
+ end
255
+
256
+ def operations_for(obj)
257
+ sorted_by_position(@context.operations_for(obj.ea_object_id))
258
+ .map { |op| build_operation(op) }
259
+ end
260
+
261
+ def enum_literals(obj)
262
+ sorted_by_position(@context.attributes_for(obj.ea_object_id))
263
+ .map { |attr| build_owned_literal(attr) }
264
+ end
265
+
266
+ # ---- Leaf element builders --------------------------------------
267
+
268
+ def build_attribute(attr)
269
+ parent_guid = parent_guid_for_attribute(attr)
270
+ ::Xmi::Uml::OwnedAttribute.new(
271
+ type: "uml:Property",
272
+ id: @context.xmi_id_for(attr),
273
+ name: attr.name,
274
+ visibility: Visibility.from_scope(attr.scope),
275
+ is_static: Visibility.boolean_from_flag(attr.isstatic),
276
+ is_ordered: Visibility.boolean_from_flag(attr.isordered),
277
+ is_derived: Visibility.boolean_from_flag(attr.derived),
278
+ uml_type: type_reference_model(attr.type, attr.classifier),
279
+ upper_value: build_upper_value(attr.upperbound, seed: "mult-attr-#{attr.id}-upper", parent_guid: parent_guid),
280
+ lower_value: build_lower_value(attr.lowerbound, seed: "mult-attr-#{attr.id}-lower", parent_guid: parent_guid),
281
+ )
282
+ end
283
+
284
+ def build_operation(op)
285
+ ::Xmi::Uml::OwnedOperation.new(
286
+ id: @context.xmi_id_for(op),
287
+ name: op.name,
288
+ visibility: Visibility.from_scope(op.scope),
289
+ is_static: Visibility.boolean_from_flag(op.isstatic),
290
+ is_abstract: Visibility.boolean_from_flag(op.abstract),
291
+ is_query: Visibility.boolean_from_flag(op.pure),
292
+ concurrency: op.concurrency&.downcase,
293
+ owned_parameter: operation_parameters(op),
294
+ )
295
+ end
296
+
297
+ def build_owned_literal(attr)
298
+ ::Xmi::Uml::OwnedLiteral.new(
299
+ type: "uml:EnumerationLiteral",
300
+ id: @context.xmi_id_for(attr),
301
+ name: attr.name,
302
+ )
303
+ end
304
+
305
+ def build_generalization(conn)
306
+ parent = @context.object_by_id(conn.end_object_id)
307
+ return nil unless parent
308
+
309
+ ::Xmi::Uml::AssociationGeneralization.new(
310
+ type: "uml:Generalization",
311
+ id: @context.xmi_id_for(conn),
312
+ general: @context.xmi_id_for(parent),
313
+ )
314
+ end
315
+
316
+ def build_interface_realization(conn)
317
+ supplier = @context.object_by_id(conn.end_object_id)
318
+ ::Xmi::Uml::InterfaceRealization.new(
319
+ type: "uml:InterfaceRealization",
320
+ id: @context.xmi_id_for(conn),
321
+ name: conn.name,
322
+ client: @context.xmi_id_for(conn.start_object_id),
323
+ supplier: supplier ? @context.xmi_id_for(supplier) : nil,
324
+ contract: supplier ? @context.xmi_id_for(supplier) : nil,
325
+ )
326
+ end
327
+
328
+ def build_dependency(conn)
329
+ client = @context.object_by_id(conn.start_object_id)
330
+ supplier = @context.object_by_id(conn.end_object_id)
331
+ return nil unless client && supplier
332
+
333
+ ::Xmi::Uml::PackagedElement.new(
334
+ type: "uml:Dependency",
335
+ id: @context.xmi_id_for(conn),
336
+ client: @context.xmi_id_for(client),
337
+ supplier: @context.xmi_id_for(supplier),
338
+ )
339
+ end
340
+
341
+ # Sparx serialisation order for `uml:Association` is
342
+ # destination member-end first, source member-end second.
343
+ # Reordering these breaks round-trip fidelity with Sparx EA
344
+ # (the importer treats the first member-end as the destination
345
+ # role). Don't reorder without verifying against a Sparx
346
+ # round-trip fixture.
347
+ def build_association(conn)
348
+ dest_end = build_association_end(conn, side: :destination)
349
+ src_end = build_association_end(conn, side: :source)
350
+
351
+ ::Xmi::Uml::PackagedElement.new(
352
+ type: "uml:Association",
353
+ id: @context.xmi_id_for(conn),
354
+ name: conn.name,
355
+ member_ends: [
356
+ ::Xmi::Uml::MemberEnd.new(idref: dest_end.xmi_id),
357
+ ::Xmi::Uml::MemberEnd.new(idref: src_end.xmi_id),
358
+ ],
359
+ owned_end: [dest_end.model, src_end.model],
360
+ )
361
+ end
362
+
363
+ def build_association_end(conn, side:)
364
+ end_id = @context.end_xmi_id_for(@context.xmi_id_for(conn), side: side)
365
+ target_id = side == :source ? conn.start_object_id : conn.end_object_id
366
+ target_obj = @context.object_by_id(target_id)
367
+ target_ref = target_obj ? @context.xmi_id_for(target_obj) : nil
368
+ bounds = Cardinality.parse(cardinality_for(conn, side))
369
+ containment = containment_for(conn, side)
370
+
371
+ model = ::Xmi::Uml::OwnedEnd.new(
372
+ type: "uml:Property",
373
+ id: end_id,
374
+ name: role_name_for(conn, side),
375
+ visibility: visibility_for_end(conn, side),
376
+ aggregation: Visibility.aggregation_from_containment(containment),
377
+ association: @context.xmi_id_for(conn),
378
+ uml_type: target_ref ? ::Xmi::Uml::Type.new(idref: target_ref) : nil,
379
+ upper_value: build_upper_value(bounds[:upper], seed: "mult-#{conn.connector_id}-#{side}-upper", parent_guid: conn.ea_guid),
380
+ lower_value: build_lower_value(bounds[:lower], seed: "mult-#{conn.connector_id}-#{side}-lower", parent_guid: conn.ea_guid),
381
+ )
382
+
383
+ AssociationEnd.new(end_id, model)
384
+ end
385
+
386
+ # ---- Multiplicity helpers ---------------------------------------
387
+
388
+ # Always emit both bounds — UML defaults (lower=0, upper=-1) are
389
+ # used when the EA field is blank. Matches real Sparx XMI, which
390
+ # never omits `<upperValue>`/`<lowerValue>` on a Property.
391
+ def build_upper_value(raw, seed:, parent_guid:)
392
+ ::Xmi::Uml::UpperValue.new(
393
+ type: "uml:LiteralUnlimitedNatural",
394
+ id: @context.id_allocator.allocate(
395
+ prefix: IdAllocator::LITERAL_INTEGER,
396
+ seed: seed,
397
+ parent_guid: parent_guid,
398
+ ),
399
+ value: Cardinality.normalize_upper(raw),
400
+ )
401
+ end
402
+
403
+ def build_lower_value(raw, seed:, parent_guid:)
404
+ ::Xmi::Uml::LowerValue.new(
405
+ type: "uml:LiteralInteger",
406
+ id: @context.id_allocator.allocate(
407
+ prefix: IdAllocator::LITERAL_INTEGER,
408
+ seed: seed,
409
+ parent_guid: parent_guid,
410
+ ),
411
+ value: Cardinality.normalize_lower(raw),
412
+ )
413
+ end
414
+
415
+ # ---- Operation parameters ---------------------------------------
416
+
417
+ def operation_parameters(op)
418
+ params = sorted_by_position(@context.params_for_operation(op.operationid))
419
+ .reject(&:return?)
420
+ .map { |p| build_owned_parameter(p) }
421
+ params << build_return_parameter(op) if op.type && !op.type.empty?
422
+ params
423
+ end
424
+
425
+ def build_owned_parameter(param)
426
+ ::Xmi::Uml::OwnedParameter.new(
427
+ id: @context.xmi_id_for(param),
428
+ name: param.name,
429
+ direction: param.kind&.downcase,
430
+ )
431
+ end
432
+
433
+ def build_return_parameter(op)
434
+ ::Xmi::Uml::OwnedParameter.new(
435
+ id: @context.id_allocator.allocate(
436
+ prefix: IdAllocator::RETURN_PARAMETER,
437
+ seed: "return-#{op.operationid}",
438
+ parent_guid: op.ea_guid,
439
+ ),
440
+ name: "return",
441
+ direction: "return",
442
+ )
443
+ end
444
+
445
+ # ---- Shared helpers ---------------------------------------------
446
+
447
+ def class_xmi_type(obj)
448
+ obj.interface? ? "uml:Interface" : "uml:Class"
449
+ end
450
+
451
+ def inheritance_connectors(obj, type)
452
+ @context.connectors_for(obj.ea_object_id).select do |conn|
453
+ conn.start_object_id == obj.ea_object_id && conn.connector_type == type
454
+ end
455
+ end
456
+
457
+ # Realization connectors owned by this class — those where
458
+ # this object is the source (client) and the connector type
459
+ # is Realization. Each emits an `<interfaceRealization>`.
460
+ def realization_connectors(obj)
461
+ inheritance_connectors(obj, "Realization")
462
+ end
463
+
464
+ def classifiers_in(pkg)
465
+ @context.objects_in_package(pkg.package_id)
466
+ .reject { |o| note?(o) || package_object?(o) }
467
+ end
468
+
469
+ def notes_in(pkg)
470
+ @context.objects_in_package(pkg.package_id).select { |o| note?(o) }
471
+ end
472
+
473
+ def note?(obj)
474
+ obj.object_type == "Note" || obj.object_type == "Text"
475
+ end
476
+
477
+ def package_object?(obj)
478
+ obj.object_type == "Package"
479
+ end
480
+
481
+ def sorted_by_position(records)
482
+ records.sort_by { |r| [r.sort_position, r.name.to_s] }
483
+ end
484
+
485
+ def primitive?(obj)
486
+ obj.object_type == "PrimitiveType" ||
487
+ (obj.gentype == "Java" && obj.stereotype_is?("primitive"))
488
+ end
489
+
490
+ def type_reference(type_name, classifier_guid)
491
+ return nil if type_name.nil? || type_name.empty?
492
+
493
+ if classifier_guid
494
+ GuidFormat.ea_guid_to_xmi_id(classifier_guid)
495
+ else
496
+ "EAnone_#{type_name}"
497
+ end
498
+ end
499
+
500
+ def type_reference_model(type_name, classifier_guid)
501
+ ref = type_reference(type_name, classifier_guid)
502
+ ref ? ::Xmi::Uml::Type.new(idref: ref) : nil
503
+ end
504
+
505
+ def cardinality_for(conn, side)
506
+ side == :source ? conn.sourcecard : conn.destcard
507
+ end
508
+
509
+ def role_name_for(conn, side)
510
+ side == :source ? conn.sourcerole : conn.destrole
511
+ end
512
+
513
+ def containment_for(conn, side)
514
+ side == :source ? conn.sourcecontainment : conn.destcontainment
515
+ end
516
+
517
+ # EA's t_connector does not expose per-end visibility (the
518
+ # source/dest scopes are stored only on the role's target
519
+ # object, which has its own visibility). Leave ownedEnd
520
+ # visibility unset unless a future schema change exposes it.
521
+ def visibility_for_end(_conn, _side)
522
+ nil
523
+ end
524
+
525
+ # The owning element for an attribute's synthesised IDs is the
526
+ # attribute's classifier (parent object), not the attribute
527
+ # itself — Sparx encodes the parent class GUID in the suffix.
528
+ def parent_guid_for_attribute(attr)
529
+ parent = @context.object_by_id(attr.ea_object_id)
530
+ parent&.ea_guid
531
+ end
532
+
533
+ # InstanceSpecification classifier reference. EA stores this
534
+ # directly in t_object.classifier as the classifier's
535
+ # ea_object_id (NOT pdata1, which is a separate column used
536
+ # for other purposes on InstanceSpecification rows).
537
+ def classifier_ref_for(obj)
538
+ classifier_id = obj.classifier.to_i
539
+ return nil if classifier_id.zero?
540
+
541
+ classifier = @context.object_by_id(classifier_id)
542
+ classifier ? @context.xmi_id_for(classifier) : nil
543
+ end
544
+
545
+ # InstanceSpecification slots, parsed from EA's RunState column.
546
+ # Each `@VAR;Variable=<name>;Value=<v>;Op=<op>;@ENDVAR;` block
547
+ # becomes one UML Slot with an OpaqueExpression value. The
548
+ # `definingFeature` is resolved by looking up the named
549
+ # attribute on the instance's classifier.
550
+ def slots_for(obj)
551
+ RunState.parse(obj.runstate).map do |binding|
552
+ build_slot(obj, binding)
553
+ end
554
+ end
555
+
556
+ def build_slot(instance, binding)
557
+ ::Xmi::Uml::Slot.new(
558
+ type: "uml:Slot",
559
+ id: slot_id_for(instance, binding),
560
+ defining_feature: defining_feature_for(instance, binding),
561
+ value: [build_slot_value(instance, binding)],
562
+ )
563
+ end
564
+
565
+ def build_slot_value(instance, binding)
566
+ ::Xmi::Uml::OpaqueExpression.new(
567
+ type: "uml:OpaqueExpression",
568
+ id: opaque_expression_id_for(instance, binding),
569
+ body_attribute: binding.body,
570
+ )
571
+ end
572
+
573
+ def slot_id_for(instance, binding)
574
+ @context.id_allocator.allocate(
575
+ prefix: IdAllocator::SLOT,
576
+ seed: "slot-#{instance.ea_object_id}-#{binding.variable}",
577
+ parent_guid: instance.ea_guid,
578
+ )
579
+ end
580
+
581
+ def opaque_expression_id_for(instance, binding)
582
+ @context.id_allocator.allocate(
583
+ prefix: IdAllocator::OPAQUE_EXPRESSION,
584
+ seed: "oe-#{instance.ea_object_id}-#{binding.variable}",
585
+ parent_guid: instance.ea_guid,
586
+ )
587
+ end
588
+
589
+ # Resolve the definingFeature EAID for a RunState binding by
590
+ # looking up the named attribute on the instance's classifier.
591
+ # Returns nil when the classifier or attribute can't be found —
592
+ # the slot still emits, just without the definingFeature ref.
593
+ def defining_feature_for(instance, binding)
594
+ classifier_id = instance.classifier.to_i
595
+ return nil if classifier_id.zero?
596
+
597
+ classifier = @context.object_by_id(classifier_id)
598
+ return nil unless classifier
599
+
600
+ attr = @context.attributes_for(classifier.ea_object_id)
601
+ .find { |a| a.name == binding.variable }
602
+ attr ? @context.xmi_id_for(attr) : nil
603
+ end
604
+ end
605
+ end
606
+ end
607
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Transformers
5
+ module QeaToXmi
6
+ # Pure-function mapper from EA's integer scope/containment codes
7
+ # to UML visibility / aggregation wire strings.
8
+ #
9
+ # EA stores visibility as an integer in `t_attribute.scope`,
10
+ # `t_operation.scope`, `t_object.scope`. The encoding:
11
+ #
12
+ # 0 → Public
13
+ # 1 → Private
14
+ # 2 → Protected
15
+ # 3 → Package
16
+ #
17
+ # EA stores aggregation kind in `t_connector.sourcecontainment`
18
+ # and `t_connector.destcontainment`. The encoding:
19
+ #
20
+ # 0 → None (omitted)
21
+ # 1 → Shared (UML aggregation="shared")
22
+ # 2 → Composite (UML aggregation="composite")
23
+ #
24
+ # Wire-side values are lower-case per the UML XMI schema.
25
+ module Visibility
26
+ SCOPE_MAP = {
27
+ 0 => "public",
28
+ 1 => "private",
29
+ 2 => "protected",
30
+ 3 => "package",
31
+ }.freeze
32
+
33
+ AGGREGATION_MAP = {
34
+ 0 => nil,
35
+ 1 => "shared",
36
+ 2 => "composite",
37
+ }.freeze
38
+
39
+ module_function
40
+
41
+ # @param raw [String, Integer, nil]
42
+ # @return [String, nil] UML visibility token, or nil if EA's
43
+ # scope field is blank / unrecognised.
44
+ def from_scope(raw)
45
+ return nil if raw.nil? || raw.to_s.strip.empty?
46
+
47
+ key = raw.to_i
48
+ SCOPE_MAP[key]
49
+ end
50
+
51
+ # @param raw [String, Integer, nil]
52
+ # @return [String, nil] UML aggregation token, or nil if EA's
53
+ # containment field indicates no aggregation.
54
+ def aggregation_from_containment(raw)
55
+ return nil if raw.nil? || raw.to_s.strip.empty?
56
+
57
+ key = raw.to_i
58
+ AGGREGATION_MAP[key]
59
+ end
60
+
61
+ # @param raw [String, Integer, nil] EA's abstract flag ("1"/"0")
62
+ # @return [Boolean, nil] true / false, or nil if unspecified.
63
+ # Returns actual Ruby booleans (not strings) — the xmi gem's
64
+ # `is_*` attributes are typed as `:boolean`.
65
+ def boolean_from_flag(raw)
66
+ return nil if raw.nil? || raw.to_s.strip.empty?
67
+
68
+ raw.to_s == "1"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end