ea 0.5.2 → 0.6.0

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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.complete/01-t-xref-parsing.md +1 -1
  3. data/TODO.complete/02-per-label-geometry.md +1 -1
  4. data/TODO.complete/03-object-properties-constraints.md +1 -1
  5. data/TODO.complete/04-operation-params.md +1 -1
  6. data/TODO.complete/05-pdata-flag-matrix.md +1 -1
  7. data/TODO.complete/06-emf-image-rendering.md +1 -1
  8. data/TODO.complete/07-auxiliary-tables.md +1 -1
  9. data/TODO.complete/10-mdg-stereotype-registry.md +1 -1
  10. data/TODO.complete/11-gml-xsd-generation.md +1 -1
  11. data/TODO.complete/13-shapescript-interpreter.md +12 -23
  12. data/TODO.complete/20-cli-diff.md +1 -1
  13. data/TODO.complete/21-cli-render.md +1 -1
  14. data/TODO.complete/22-cli-export.md +1 -1
  15. data/TODO.complete/23-cli-mdg.md +1 -1
  16. data/TODO.complete/30-headerlines-ocp-refactor.md +1 -1
  17. data/TODO.complete/32-spec-coverage.md +1 -1
  18. data/TODO.complete/33-blocked-parity-gaps.md +1 -1
  19. data/TODO.complete/36-stereotype-icon-wiring.md +1 -1
  20. data/TODO.complete/37-cli-lint.md +1 -1
  21. data/TODO.complete/38-cli-query.md +1 -1
  22. data/TODO.complete/39-cli-info.md +1 -1
  23. data/TODO.complete/40-ocl-evaluator.md +1 -1
  24. data/TODO.complete/41-round-trip-spec.md +1 -1
  25. data/TODO.complete/42-html-diff-report.md +1 -1
  26. data/TODO.complete/43-json-schema-export.md +1 -1
  27. data/TODO.complete/44-remaining-qea-tables.md +1 -1
  28. data/TODO.complete/45-shapescript-extensions.md +1 -1
  29. data/TODO.complete/46-xsd-consolidation.md +1 -1
  30. data/TODO.complete/47-headerlines-legacy-removal.md +1 -1
  31. data/TODO.complete/51-remove-respond-to.md +42 -15
  32. data/TODO.complete/52-xsd-decouple-fixtures.md +1 -1
  33. data/TODO.complete/54-wire-stereotype-icon-renderer.md +1 -1
  34. data/TODO.complete/55-wire-shapescript.md +1 -1
  35. data/TODO.complete/56-wire-emf-renderer.md +1 -1
  36. data/TODO.complete/57-wire-ocl-evaluator.md +1 -1
  37. data/TODO.complete/58-diff-modifications.md +1 -1
  38. data/TODO.complete/59-delete-toy-xmi-export.md +1 -1
  39. data/TODO.complete/60-curate-json-export.md +1 -1
  40. data/TODO.complete/61-plantuml-relationships.md +1 -1
  41. data/TODO.complete/62-changelog.md +1 -1
  42. data/TODO.complete/63-dependabot-fix.md +1 -1
  43. data/TODO.complete/64-qeatoxmi-connectors.md +9 -24
  44. data/TODO.complete/65-qeatoxmi-diagrams.md +9 -23
  45. data/TODO.complete/66-qeatoxmi-style-tags-docs.md +25 -18
  46. data/TODO.complete/67-ocl-collection-ops.md +12 -12
  47. data/TODO.complete/68-ocl-comparison-ops.md +16 -11
  48. data/TODO.complete/69-spec-new-models.md +13 -18
  49. data/TODO.complete/70-specs-for-all-models.md +21 -0
  50. data/TODO.complete/71-autoload-audit.md +26 -0
  51. data/TODO.complete/72-extension-serializer-ocp-naming.md +26 -0
  52. data/TODO.complete/73-ocl-validator-latent-bugs.md +50 -0
  53. data/TODO.complete/74-visibility-symbol-dry.md +39 -0
  54. data/TODO.complete/75-database-table-collection-registry.md +32 -0
  55. data/TODO.complete/76-stale-todo-cleanup.md +52 -0
  56. data/TODO.complete/77-eliminate-banned-patterns-from-specs.md +55 -0
  57. data/TODO.complete/78-mdg-stereotype-emission.md +66 -0
  58. data/TODO.complete/79-equivalence-fixes.md +57 -0
  59. data/TODO.complete/80-performance-extension-serializer-indexes.md +43 -0
  60. data/TODO.complete/81-dry-scope-guid-extraction.md +33 -0
  61. data/TODO.complete/82-public-send-audit-conclusion.md +25 -0
  62. data/TODO.complete/83-dry-shared-xml-escape-guid-format.md +52 -0
  63. data/TODO.complete/84-ocp-markers-shape-registry.md +54 -0
  64. data/TODO.complete/85-frozen-literal-rubocop-normalize-type-spec.md +38 -0
  65. data/TODO.complete/86-frozen-sweep-lint-query-specs.md +55 -0
  66. data/TODO.complete/STATUS.md +26 -5
  67. data/lib/ea/cli/app.rb +1 -0
  68. data/lib/ea/cli/command/export.rb +10 -1
  69. data/lib/ea/cli/command/mdg.rb +1 -10
  70. data/lib/ea/cli/command/render.rb +2 -1
  71. data/lib/ea/diff/html_reporter.rb +1 -5
  72. data/lib/ea/export/json/generator.rb +5 -6
  73. data/lib/ea/export/plantuml/generator.rb +5 -3
  74. data/lib/ea/guid_format.rb +91 -0
  75. data/lib/ea/mdg/document.rb +2 -1
  76. data/lib/ea/mdg/loader.rb +2 -1
  77. data/lib/ea/mdg/registry.rb +57 -2
  78. data/lib/ea/mdg/xml/applies_to.rb +1 -1
  79. data/lib/ea/mdg/xml/apply.rb +1 -1
  80. data/lib/ea/mdg/xml/content.rb +1 -1
  81. data/lib/ea/mdg/xml/documentation.rb +1 -1
  82. data/lib/ea/mdg/xml/stereotype.rb +1 -1
  83. data/lib/ea/mdg/xml/stereotypes.rb +1 -1
  84. data/lib/ea/mdg/xml/tag.rb +1 -1
  85. data/lib/ea/mdg/xml/tagged_values.rb +1 -1
  86. data/lib/ea/mdg/xml/technology.rb +1 -1
  87. data/lib/ea/mdg/xml/uml_profile.rb +1 -1
  88. data/lib/ea/mdg/xml/uml_profiles.rb +1 -1
  89. data/lib/ea/mdg/xml.rb +2 -1
  90. data/lib/ea/mdg.rb +2 -1
  91. data/lib/ea/model/ghost_label.rb +2 -1
  92. data/lib/ea/ocl/evaluator.rb +4 -2
  93. data/lib/ea/qea/database.rb +57 -2
  94. data/lib/ea/qea/factory/reference_resolver.rb +1 -1
  95. data/lib/ea/qea/models/ea_attribute.rb +1 -17
  96. data/lib/ea/qea/models/ea_connector.rb +3 -2
  97. data/lib/ea/qea/models/ea_object.rb +14 -9
  98. data/lib/ea/qea/models/ea_operation.rb +1 -17
  99. data/lib/ea/qea/models/ea_xref.rb +3 -0
  100. data/lib/ea/qea/models/scope_predicate.rb +29 -0
  101. data/lib/ea/qea/models.rb +1 -0
  102. data/lib/ea/qea/repositories/base_repository.rb +4 -1
  103. data/lib/ea/qea/validation/base_validator.rb +3 -48
  104. data/lib/ea/qea/validation/database/ocl_constraint_validator.rb +23 -13
  105. data/lib/ea/shapescript/renderer.rb +1 -3
  106. data/lib/ea/sources/qea/diagram_builder.rb +1 -0
  107. data/lib/ea/sources/qea/id_normalizer.rb +2 -2
  108. data/lib/ea/svg/ea_emitter/background.rb +2 -1
  109. data/lib/ea/svg/ea_emitter/element/attribute_line_builder.rb +1 -6
  110. data/lib/ea/svg/ea_emitter/element/operation_renderer.rb +2 -7
  111. data/lib/ea/svg/ea_emitter/elements.rb +1 -3
  112. data/lib/ea/svg/ea_emitter/ghost_labels.rb +2 -1
  113. data/lib/ea/svg/ea_emitter/label/end_label.rb +1 -6
  114. data/lib/ea/svg/ea_emitter/marker/package_import.rb +1 -1
  115. data/lib/ea/svg/ea_emitter/markers.rb +52 -23
  116. data/lib/ea/svg/ea_emitter/text_renderer.rb +1 -4
  117. data/lib/ea/svg/ea_emitter/visibility_symbol.rb +37 -0
  118. data/lib/ea/svg/ea_emitter.rb +1 -0
  119. data/lib/ea/svg/parity/source.rb +0 -2
  120. data/lib/ea/svg/parity/suite.rb +0 -1
  121. data/lib/ea/transformers/qea_to_xmi/cardinality.rb +40 -28
  122. data/lib/ea/transformers/qea_to_xmi/context.rb +35 -1
  123. data/lib/ea/transformers/qea_to_xmi/extension_serializer.rb +686 -0
  124. data/lib/ea/transformers/qea_to_xmi/guid_format.rb +5 -48
  125. data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +55 -54
  126. data/lib/ea/transformers/qea_to_xmi/primitive_types.rb +105 -0
  127. data/lib/ea/transformers/qea_to_xmi/profile_serializer.rb +163 -0
  128. data/lib/ea/transformers/qea_to_xmi/run_state.rb +7 -6
  129. data/lib/ea/transformers/qea_to_xmi/transformer.rb +396 -185
  130. data/lib/ea/transformers/qea_to_xmi/visibility.rb +30 -19
  131. data/lib/ea/transformers/qea_to_xmi.rb +5 -2
  132. data/lib/ea/transformers.rb +7 -2
  133. data/lib/ea/validation.rb +1 -1
  134. data/lib/ea/version.rb +1 -1
  135. data/lib/ea/xmi/liquid_drops/attribute_drop.rb +12 -4
  136. data/lib/ea/xmi/liquid_drops/klass_drop.rb +1 -2
  137. data/lib/ea/xmi/lookup_service.rb +4 -0
  138. data/lib/ea/xmi/parser.rb +30 -1
  139. data/lib/ea/xml_escape.rb +21 -0
  140. data/lib/ea.rb +2 -0
  141. metadata +28 -11
  142. data/lutaml-uml-0.5.1.gem +0 -0
@@ -0,0 +1,686 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Transformers
5
+ module QeaToXmi
6
+ # Serializes the EA-specific content of `<xmi:Extension>`.
7
+ #
8
+ # The xmi gem models the structural XMI tree (model, packages,
9
+ # classes, attributes) but does not represent EA's per-element
10
+ # extension metadata. This class produces the inner XML for the
11
+ # `<xmi:Extension>` element as three sibling sections:
12
+ #
13
+ # <elements> — one <element> per package/classifier, carrying
14
+ # <model>, <properties documentation="...">,
15
+ # <style appearance="..."> and nested <tags>.
16
+ # <connectors> — one <connector> per relationship, with
17
+ # source/target <model> blocks.
18
+ # <diagrams> — one <diagram> per diagram, with placed
19
+ # <element> children referencing subjects.
20
+ #
21
+ # The Transformer injects this string into the serialized XMI's
22
+ # empty `<xmi:Extension>` placeholder.
23
+ #
24
+ # Sparx XMI is the canonical shape; the xmi gem's parser does not
25
+ # model these sections, so output containing them is not
26
+ # round-trippable through `Xmi::Sparx::Root.parse_xml`.
27
+ class ExtensionSerializer
28
+ def initialize(database, context)
29
+ @database = database
30
+ @context = context
31
+ end
32
+
33
+ # @return [String] inner XML for <xmi:Extension>; empty if no
34
+ # elements/connectors/diagrams to emit.
35
+ def call
36
+ sections = [
37
+ build_elements_section,
38
+ build_connectors_section,
39
+ build_primitivetypes_section,
40
+ build_diagrams_section
41
+ ].reject(&:empty?)
42
+ sections.join("\n")
43
+ end
44
+
45
+ private
46
+
47
+ # ---- <elements> section -----------------------------------------
48
+
49
+ def build_elements_section
50
+ entries = element_entries
51
+ return "" if entries.empty?
52
+
53
+ "\t\t<elements>\n#{entries.join("\n")}\n\t\t</elements>"
54
+ end
55
+
56
+ # One <element> block per package and per classifier object.
57
+ # Notes/Text/ProxyConnector objects are skipped — they have no
58
+ # standalone representation in the extension's <elements> list.
59
+ def element_entries
60
+ packages = @database.collections[:packages] || []
61
+ objects = @database.collections[:objects] || []
62
+ package_entries = packages.map { |pkg| package_element_xml(pkg) }
63
+ object_entries = objects.filter_map { |obj| object_element_xml(obj) }
64
+ (package_entries + object_entries).compact
65
+ end
66
+
67
+ def package_element_xml(pkg)
68
+ idref = @context.xmi_id_for(pkg, prefix: "EAPK")
69
+ return nil unless idref
70
+
71
+ name = pkg.name.to_s
72
+ scope = "public"
73
+ lines = [
74
+ %(\t\t<element xmi:idref="#{idref}" xmi:type="uml:Package" name="#{escape(name)}" scope="#{scope}">),
75
+ model_for_package(pkg),
76
+ properties_for(record_documentation(pkg.notes), stype: "Package", scope: scope),
77
+ style_for_package(pkg),
78
+ tags_block_for(pkg.ea_guid),
79
+ "\t\t</element>"
80
+ ]
81
+ lines.join("\n")
82
+ end
83
+
84
+ def object_element_xml(obj)
85
+ return nil if skip_object?(obj)
86
+
87
+ idref = @context.xmi_id_for(obj)
88
+ return nil unless idref
89
+
90
+ uml_type = uml_type_for(obj)
91
+ name = obj.name.to_s
92
+ scope = (obj.scope || "Public").downcase
93
+ lines = [
94
+ %(\t\t<element xmi:idref="#{idref}" xmi:type="#{uml_type}" name="#{escape(name)}" scope="#{scope}">),
95
+ model_for_object(obj),
96
+ properties_for(record_documentation(obj.note), stype: obj.object_type, scope: scope,
97
+ is_abstract: obj.abstract?),
98
+ style_for_object(obj),
99
+ tags_block_for(obj.ea_guid),
100
+ xrefs_xml("\t\t\t", obj.ea_guid, "element property"),
101
+ attributes_block_for(obj),
102
+ operations_block_for(obj),
103
+ "\t\t</element>"
104
+ ]
105
+ lines.join("\n")
106
+ end
107
+
108
+ # EA object types that are diagram-only annotations or internal
109
+ # plumbing — they don't get a standalone <element> entry.
110
+ SKIP_OBJECT_TYPES = %w[Note Text ProxyConnector Boundary].freeze
111
+
112
+ def skip_object?(obj)
113
+ obj.object_type.nil? || SKIP_OBJECT_TYPES.include?(obj.object_type)
114
+ end
115
+
116
+ # OCP registry: EA object type → UML XMI type.
117
+ # Adding a new mapping = adding one entry, not editing a
118
+ # case/when branch.
119
+ UML_TYPE_FOR = {
120
+ "Class" => "uml:Class",
121
+ "Interface" => "uml:Interface",
122
+ "Enumeration" => "uml:Enumeration",
123
+ "DataType" => "uml:DataType",
124
+ "PrimitiveType" => "uml:PrimitiveType",
125
+ "Object" => "uml:Object",
126
+ "Signal" => "uml:Signal",
127
+ "Package" => "uml:Package",
128
+ "Association" => "uml:Association"
129
+ }.freeze
130
+
131
+ def uml_type_for(obj)
132
+ UML_TYPE_FOR[obj.object_type] || obj.object_type.to_s
133
+ end
134
+
135
+ # ---- <model> / <properties> / <style> ---------------------------
136
+
137
+ def model_for_package(pkg)
138
+ parent = parent_package_ref(pkg)
139
+ owner = parent
140
+ attrs = [
141
+ %(package2="#{@context.xmi_id_for(pkg)}")
142
+ ]
143
+ attrs << %(package="#{parent}") if parent
144
+ attrs << %(owner="#{owner}") if owner
145
+ attrs << %(tpos="#{pkg.tpos}") if pkg.tpos
146
+ attrs << %(ea_localid="#{pkg.package_id}")
147
+ attrs << %(ea_eleType="package")
148
+ "\t\t\t<model #{attrs.join(" ")}/>"
149
+ end
150
+
151
+ def model_for_object(obj)
152
+ pkg_ref = owner_package_ref(obj)
153
+ attrs = []
154
+ attrs << %(package="#{pkg_ref}") if pkg_ref
155
+ attrs << %(tpos="#{obj.tpos}") if obj.tpos
156
+ attrs << %(ea_localid="#{obj.ea_object_id}")
157
+ attrs << %(ea_eleType="element")
158
+ "\t\t\t<model #{attrs.join(" ")}/>"
159
+ end
160
+
161
+ def properties_for(documentation, stype:, scope:, is_abstract: nil)
162
+ attrs = []
163
+ attrs << %(documentation="#{escape(documentation)}") unless documentation.nil? || documentation.empty?
164
+ attrs << %(isSpecification="false")
165
+ attrs << %(sType="#{stype}")
166
+ attrs << %(nType="0")
167
+ attrs << %(scope="#{scope}")
168
+ attrs << %(isAbstract="#{is_abstract ? "true" : "false"}")
169
+ "\t\t\t<properties #{attrs.join(" ")}/>"
170
+ end
171
+
172
+ def style_for_package(_pkg)
173
+ # Packages in t_package have no style column; EA emits a
174
+ # default style block for them.
175
+ default = "BackColor=-1;BorderColor=-1;BorderWidth=-1;" \
176
+ "FontColor=-1;VSwimLanes=1;HSwimLanes=1;BorderStyle=0;"
177
+ %(\t\t\t<style appearance="#{escape(default)}"/>)
178
+ end
179
+
180
+ def style_for_object(obj)
181
+ appearance = obj.style && obj.style.empty? ? default_object_style : obj.style
182
+ %(\t\t\t<style appearance="#{escape(appearance.to_s)}"/>)
183
+ end
184
+
185
+ def default_object_style
186
+ "BackColor=-1;BorderColor=-1;BorderWidth=-1;" \
187
+ "FontColor=-1;VSwimLanes=1;HSwimLanes=1;BorderStyle=0;"
188
+ end
189
+
190
+ # ---- <tags> -----------------------------------------------------
191
+
192
+ # Tags section for one model element. Looks up tagged values
193
+ # whose ElementID matches the given GUID. Returns an empty
194
+ # <tags/> when there are none — matches EA's output shape.
195
+ def tags_block_for(element_guid)
196
+ tags = tagged_values_for(element_guid)
197
+ if tags.empty?
198
+ "\t\t\t<tags/>"
199
+ else
200
+ inner = tags.map { |tv| tag_xml(tv) }.join("\n")
201
+ "\t\t\t<tags>\n#{inner}\n\t\t\t</tags>"
202
+ end
203
+ end
204
+
205
+ def tag_xml(tv)
206
+ tag_id = GuidFormat.ea_guid_to_xmi_id(tv.property_id)
207
+ tag_name = tv.tag_name.to_s
208
+ value = tv.parsed_value
209
+ model_element = element_ref_for_tag(tv)
210
+ attrs = [%(xmi:id="#{tag_id}"), %(name="#{escape(tag_name)}")]
211
+ attrs << %(value="#{escape(value)}") unless value.nil? || value.empty?
212
+ attrs << %(modelElement="#{model_element}") if model_element
213
+ "\t\t\t\t<tag #{attrs.join(" ")}/>"
214
+ end
215
+
216
+ def tagged_values_for(element_guid)
217
+ return [] unless element_guid
218
+
219
+ @database.tagged_values_for_element(element_guid)
220
+ end
221
+
222
+ def element_ref_for_tag(tv)
223
+ return nil unless tv.element_id
224
+
225
+ GuidFormat.ea_guid_to_xmi_id(tv.element_id)
226
+ end
227
+
228
+ # ---- <attributes> block -----------------------------------------
229
+
230
+ # Classifier types that own attributes in their <attributes>
231
+ # extension block. Enumerations use <ownedLiteral> instead;
232
+ # InstanceSpecifications have no attributes.
233
+ ATTRIBUTE_OWNERS = %w[Class Interface DataType PrimitiveType].freeze
234
+
235
+ def attributes_block_for(obj)
236
+ return "" unless ATTRIBUTE_OWNERS.include?(obj.object_type)
237
+
238
+ attrs = @context.attributes_for(obj.ea_object_id)
239
+ return "" if attrs.empty?
240
+
241
+ inner = attrs.map { |attr| attribute_xml(attr) }.join("\n")
242
+ "\t\t\t<attributes>\n#{inner}\n\t\t\t</attributes>"
243
+ end
244
+
245
+ def attribute_xml(attr)
246
+ idref = @context.xmi_id_for(attr)
247
+ scope = attribute_scope(attr)
248
+ lines = [
249
+ %(\t\t\t\t<attribute xmi:idref="#{idref}" name="#{escape(attr.name.to_s)}" scope="#{scope}">),
250
+ "\t\t\t\t\t<initial/>",
251
+ documentation_element(attr.notes),
252
+ "\t\t\t\t\t<model ea_localid=\"#{attr.id}\" ea_guid=\"#{escape(attr.ea_guid.to_s)}\"/>",
253
+ attribute_properties(attr),
254
+ "\t\t\t\t\t<coords ordered=\"0\" scale=\"0\"/>",
255
+ "\t\t\t\t\t<containment containment=\"Not Specified\" position=\"#{attr.pos || 0}\"/>",
256
+ "\t\t\t\t\t<stereotype/>",
257
+ bounds_xml(attr),
258
+ "\t\t\t\t\t<options/>",
259
+ "\t\t\t\t\t<style/>",
260
+ %(\t\t\t\t\t<styleex value="#{escape(attr.styleex.to_s)}"/>),
261
+ "\t\t\t\t\t<tags/>",
262
+ xrefs_xml("\t\t\t\t\t", attr.ea_guid, "attribute property"),
263
+ "\t\t\t\t</attribute>"
264
+ ]
265
+ lines.join("\n")
266
+ end
267
+
268
+ # Shares the transformer's pair rule so the extension block and
269
+ # the UML tree can never report different multiplicities.
270
+ def bounds_xml(attr)
271
+ bounds = Cardinality.attribute_bounds(attr.lowerbound, attr.upperbound)
272
+ %(\t\t\t\t\t<bounds lower="#{escape(bounds[:lower])}" upper="#{escape(bounds[:upper])}"/>)
273
+ end
274
+
275
+ def attribute_scope(attr)
276
+ (attr.scope || "Public").downcase.capitalize
277
+ end
278
+
279
+ def attribute_properties(attr)
280
+ attrs = [
281
+ %(type="#{escape(attr.type.to_s)}"),
282
+ %(derived="#{derived_flag(attr)}"),
283
+ %(precision="#{attr.precision || 0}"),
284
+ %(collection="false"),
285
+ %(length="#{attr.length || 0}"),
286
+ %(static="#{attr.isstatic || 0}"),
287
+ %(duplicates="#{attr.allowduplicates || 0}"),
288
+ %(changeability="changeable")
289
+ ]
290
+ "\t\t\t\t\t<properties #{attrs.join(" ")}/>"
291
+ end
292
+
293
+ def derived_flag(attr)
294
+ attr.derived == "1" ? "1" : "0"
295
+ end
296
+
297
+ # ---- <operations> block -----------------------------------------
298
+
299
+ OPERATION_OWNERS = %w[Class Interface DataType PrimitiveType].freeze
300
+
301
+ def operations_block_for(obj)
302
+ return "" unless OPERATION_OWNERS.include?(obj.object_type)
303
+
304
+ ops = @context.operations_for(obj.ea_object_id)
305
+ return "" if ops.empty?
306
+
307
+ inner = ops.map { |op| operation_xml(op) }.join("\n")
308
+ "\t\t\t\t<operations>\n#{inner}\n\t\t\t\t</operations>"
309
+ end
310
+
311
+ def operation_xml(op)
312
+ idref = @context.xmi_id_for(op)
313
+ scope = operation_scope(op)
314
+ lines = [
315
+ %(\t\t\t\t<operation xmi:idref="#{idref}" name="#{escape(op.name.to_s)}" scope="#{scope}">),
316
+ %(\t\t\t\t\t<properties position="#{op.pos || 0}"/>),
317
+ "\t\t\t\t\t<stereotype/>",
318
+ "\t\t\t\t\t<model ea_guid=\"#{escape(op.ea_guid.to_s)}\" ea_localid=\"#{op.operationid}\"/>",
319
+ operation_type(op),
320
+ "\t\t\t\t\t<behaviour/>",
321
+ "\t\t\t\t\t<code/>",
322
+ "\t\t\t\t\t<style/>",
323
+ "\t\t\t\t\t<styleex/>",
324
+ *operation_tail(op)
325
+ ]
326
+ lines.join("\n")
327
+ end
328
+
329
+ def operation_tail(op)
330
+ [documentation_element(op.notes),
331
+ "\t\t\t\t\t<tags/>",
332
+ parameters_block_for(op),
333
+ xrefs_xml("\t\t\t\t\t", op.ea_guid, "operation property"),
334
+ "\t\t\t\t</operation>"]
335
+ end
336
+
337
+ def operation_scope(op)
338
+ (op.scope || "Public").downcase.capitalize
339
+ end
340
+
341
+ def operation_type(op)
342
+ attrs = [
343
+ %(type="#{escape(op.type.to_s)}"),
344
+ %(const="false"),
345
+ %(static="#{op.isstatic || 0}"),
346
+ %(isAbstract="#{op.abstract || 0}"),
347
+ %(synchronised="0"),
348
+ %(concurrency="#{escape((op.concurrency || "Sequential").to_s)}"),
349
+ %(returnarray="0"),
350
+ %(pure="#{op.pure || 0}"),
351
+ %(isQuery="#{op.pure && op.pure != 0 ? "true" : "false"}")
352
+ ]
353
+ "\t\t\t\t\t<type #{attrs.join(" ")}/>"
354
+ end
355
+
356
+ def parameters_block_for(op)
357
+ entries = [return_parameter_xml(op), *declared_parameters_xml(op)].compact
358
+ return "" if entries.empty?
359
+
360
+ "\t\t\t\t\t<parameters>\n#{entries.join("\n")}\n\t\t\t\t\t</parameters>"
361
+ end
362
+
363
+ def declared_parameters_xml(op)
364
+ @context.params_for_operation(op.operationid)
365
+ .reject(&:return?)
366
+ .map { |param| parameter_xml(param) }
367
+ end
368
+
369
+ # The trailing children of a synthesized return parameter —
370
+ # all empty, in EA's order.
371
+ RETURN_PARAMETER_TAIL = ["\t\t\t\t\t\t\t<style/>",
372
+ "\t\t\t\t\t\t\t<styleex/>",
373
+ "\t\t\t\t\t\t\t<documentation/>",
374
+ "\t\t\t\t\t\t\t<tags/>",
375
+ "\t\t\t\t\t\t\t<xrefs/>"].freeze
376
+
377
+ # EA lists the return first, as a synthesized parameter with no
378
+ # t_operationparams row behind it. Both its idref and its
379
+ # ea_guid come from the operation's own id via {GuidFormat}, so
380
+ # the two agree by construction — an untyped operation has no
381
+ # return to describe, and one whose nullable t_operation.ea_guid
382
+ # is blank has nothing to derive the ids from.
383
+ def return_parameter_xml(operation)
384
+ return nil if PrimitiveTypes.normalize_name(operation.type).empty?
385
+ return nil unless @context.identifiable?(operation)
386
+
387
+ return_id = GuidFormat.return_parameter_xmi_id(@context.xmi_id_for(operation))
388
+ [%(\t\t\t\t\t\t<parameter xmi:idref="#{return_id}" visibility="public">),
389
+ %(\t\t\t\t\t\t\t<properties pos="0" type="#{escape(operation.type.to_s)}" const="false" ) +
390
+ %(ea_guid="#{GuidFormat.xmi_id_to_ea_guid(return_id)}"/>),
391
+ *RETURN_PARAMETER_TAIL,
392
+ "\t\t\t\t\t\t</parameter>"].join("\n")
393
+ end
394
+
395
+ def parameter_xml(param)
396
+ idref = @context.xmi_id_for(param)
397
+ lines = [
398
+ %(\t\t\t\t\t\t<parameter xmi:idref="#{idref}" visibility="public">),
399
+ %(\t\t\t\t\t\t\t<properties pos="#{param.pos || 0}" type="#{escape(param.type.to_s)}" const="false" ea_guid="#{escape(param.ea_guid.to_s)}"/>),
400
+ "\t\t\t\t\t\t\t<style/>",
401
+ "\t\t\t\t\t\t\t<styleex/>",
402
+ documentation_element(param.notes),
403
+ "\t\t\t\t\t\t\t<tags/>",
404
+ "\t\t\t\t\t\t\t<xrefs/>",
405
+ "\t\t\t\t\t\t</parameter>"
406
+ ]
407
+ lines.join("\n")
408
+ end
409
+
410
+ # Emits a <documentation value="..."/> element for attribute/
411
+ # operation/parameter sub-blocks. Empty element when there's
412
+ # no note content (matches EA's output for blank notes).
413
+ def documentation_element(note)
414
+ if note.nil? || note.empty?
415
+ "\t\t\t\t\t<documentation/>"
416
+ else
417
+ %(\t\t\t\t\t<documentation value="#{escape(note)}"/>)
418
+ end
419
+ end
420
+
421
+ # ---- <connectors> section --------------------------------------
422
+
423
+ def build_connectors_section
424
+ connectors = @database.collections[:connectors] || []
425
+ return "" if connectors.empty?
426
+
427
+ inner = connectors.filter_map { |conn| connector_xml(conn) }.join("\n")
428
+ return "" if inner.empty?
429
+
430
+ "\t\t<connectors>\n#{inner}\n\t\t</connectors>"
431
+ end
432
+
433
+ def connector_xml(conn)
434
+ return nil unless conn.ea_guid
435
+
436
+ connector_id = @context.xmi_id_for(conn)
437
+ source_obj = @database.find_object(conn.start_object_id)
438
+ target_obj = @database.find_object(conn.end_object_id)
439
+ return nil unless source_obj && target_obj
440
+
441
+ name_attr = conn.name ? %( name="#{escape(conn.name)}") : ""
442
+ inner = end_xml(conn, source_obj, "source") + end_xml(conn, target_obj, "target") + connector_body_xml(conn)
443
+ %(\t\t<connector xmi:idref="#{connector_id}"#{name_attr}>\n#{inner}\t\t</connector>)
444
+ end
445
+
446
+ END_XREF_TYPE = {
447
+ "source" => "connectorSrcEnd property",
448
+ "target" => "connectorDestEnd property"
449
+ }.freeze
450
+
451
+ def end_xml(conn, obj, kind)
452
+ xmi_id = @context.xmi_id_for(obj)
453
+ lines = [
454
+ "\t\t\t<#{kind} xmi:idref=\"#{xmi_id}\">",
455
+ "\t\t\t\t<model ea_localid=\"#{obj.ea_object_id}\" " \
456
+ "type=\"#{obj.object_type}\" name=\"#{escape(obj.name.to_s)}\"/>",
457
+ "\t\t\t\t<role visibility=\"Public\" targetScope=\"instance\"/>",
458
+ "\t\t\t\t<type aggregation=\"none\" containment=\"Unspecified\"/>",
459
+ "\t\t\t\t<constraints/>",
460
+ "\t\t\t\t<modifiers isOrdered=\"false\" changeable=\"none\" isNavigable=\"false\"/>",
461
+ "\t\t\t\t<style value=\"Union=0;Derived=0;AllowDuplicates=0;Owned=0;Navigable=Unspecified;\"/>",
462
+ "\t\t\t\t<documentation/>",
463
+ xrefs_xml("\t\t\t\t", conn.ea_guid, END_XREF_TYPE.fetch(kind)),
464
+ "\t\t\t\t<tags/>",
465
+ "\t\t\t</#{kind}>"
466
+ ]
467
+ "#{lines.join("\n")}\n"
468
+ end
469
+
470
+ # Connector-level body (after source/target): model, properties,
471
+ # documentation, appearance, style, tags. Matches the reference
472
+ # EA output shape.
473
+ def connector_body_xml(conn)
474
+ ea_type = conn.connector_type || "Association"
475
+ lines = [
476
+ "\t\t\t<model ea_localid=\"#{conn.connector_id}\"/>",
477
+ %(\t\t\t<properties ea_type="#{escape(ea_type)}" direction="Unspecified"/>),
478
+ "\t\t\t<modifiers isRoot=\"false\" isLeaf=\"false\"/>",
479
+ "\t\t\t<parameterSubstitutions/>",
480
+ "\t\t\t<documentation/>",
481
+ "\t\t\t<appearance linemode=\"3\" linecolor=\"-1\" linewidth=\"0\" " \
482
+ "seqno=\"0\" headStyle=\"0\" lineStyle=\"0\"/>",
483
+ "\t\t\t<extendedProperties virtualInheritance=\"0\"/>",
484
+ "\t\t\t<style/>",
485
+ xrefs_xml("\t\t\t", conn.ea_guid, "connector property"),
486
+ "\t\t\t<tags/>"
487
+ ]
488
+ "#{lines.join("\n")}\n"
489
+ end
490
+
491
+ # ---- <xrefs> ----------------------------------------------------
492
+
493
+ # EA renders t_xref rows as a $XREFPROP mini-language string in
494
+ # a single value attribute. Derived from the reference exports:
495
+ # token order $XID $NAM $TYP $VIS [$BEH] $PAR $DES $CLT [$SUP]
496
+ # $ENDXREF; — $BEH only when Behavior is non-blank. A blank
497
+ # Supplier renders as "<none>" except on "connector property"
498
+ # rows, where EA omits the token entirely. "diagram properties"
499
+ # rows are never rendered (reference diagrams keep <xrefs/>).
500
+ def xrefs_xml(indent, ea_guid, type)
501
+ rows = @database.xrefs_for_client(ea_guid.to_s)
502
+ .select { |xref| xref.xref_type == type }
503
+ return "#{indent}<xrefs/>" if rows.empty?
504
+
505
+ value = rows.map { |xref| xref_value(xref) }.join
506
+ %(#{indent}<xrefs value="#{escape(value)}"/>)
507
+ end
508
+
509
+ def xref_value(xref)
510
+ head = "$XREFPROP=$XID=#{xref.xref_id}$XID;$NAM=#{xref.name}$NAM;" \
511
+ "$TYP=#{xref.xref_type}$TYP;$VIS=#{xref.visibility}$VIS;"
512
+ beh = xref.behavior.to_s.empty? ? "" : "$BEH=#{xref.behavior}$BEH;"
513
+ tail = "$PAR=#{xref.partition}$PAR;$DES=#{xref.description}$DES;" \
514
+ "$CLT=#{xref.client}$CLT;#{supplier_token(xref)}$ENDXREF;"
515
+ "#{head}#{beh}#{tail}"
516
+ end
517
+
518
+ def supplier_token(xref)
519
+ return "$SUP=#{xref.supplier}$SUP;" unless xref.supplier.to_s.empty?
520
+ return "" if xref.xref_type == "connector property"
521
+
522
+ "$SUP=<none>$SUP;"
523
+ end
524
+
525
+ # ---- <primitivetypes> section ----------------------------------
526
+
527
+ # EA nests every synthesized EAnone_ PrimitiveType definition
528
+ # under a fixed two-package hierarchy inside the Extension.
529
+ def build_primitivetypes_section
530
+ names = PrimitiveTypes.unresolved_names(@database)
531
+ return "" if names.empty?
532
+
533
+ ["\t\t<primitivetypes>",
534
+ primitive_package("\t\t\t", "EAPrimitiveTypesPackage", "EA_PrimitiveTypes_Package"),
535
+ primitive_package("\t\t\t\t", "EAnoneTypesPackage", "EA_none_Types_Package"),
536
+ *names.map { |name| primitive_definition(name) },
537
+ "\t\t\t\t</packagedElement>",
538
+ "\t\t\t</packagedElement>",
539
+ "\t\t</primitivetypes>"].join("\n")
540
+ end
541
+
542
+ def primitive_package(indent, id, name)
543
+ %(#{indent}<packagedElement xmi:type="uml:Package" xmi:id="#{id}" name="#{name}">)
544
+ end
545
+
546
+ def primitive_definition(name)
547
+ "\t\t\t\t\t<packagedElement xmi:type=\"uml:PrimitiveType\" " \
548
+ "xmi:id=\"#{escape(PrimitiveTypes.definition_id(name))}\" name=\"#{escape(name)}\"/>"
549
+ end
550
+
551
+ # ---- <diagrams> section ----------------------------------------
552
+
553
+ def build_diagrams_section
554
+ diagrams = @database.collections[:diagrams] || []
555
+ return "" if diagrams.empty?
556
+
557
+ inner = diagrams.filter_map { |dgm| diagram_xml(dgm) }.join("\n")
558
+ return "" if inner.empty?
559
+
560
+ "\t\t<diagrams>\n#{inner}\n\t\t</diagrams>"
561
+ end
562
+
563
+ def diagram_xml(dgm)
564
+ return nil unless dgm.ea_guid
565
+
566
+ diagram_id = @context.xmi_id_for(dgm)
567
+ pkg_guid = package_guid_for(dgm)
568
+ pkg_ref = pkg_guid ? GuidFormat.ea_guid_to_xmi_id(pkg_guid, prefix: "EAPK") : ""
569
+ elements_xml = diagram_elements_xml(dgm)
570
+ lines = [
571
+ "\t\t<diagram xmi:id=\"#{diagram_id}\">",
572
+ "\t\t\t<model package=\"#{pkg_ref}\" localID=\"#{dgm.diagram_id}\" owner=\"#{pkg_ref}\"/>",
573
+ "\t\t\t<properties name=\"#{escape(dgm.name.to_s)}\" type=\"#{escape(dgm.diagram_type.to_s)}\"/>",
574
+ elements_xml,
575
+ "\t\t</diagram>"
576
+ ]
577
+ lines.join("\n")
578
+ end
579
+
580
+ # EA emits object placements (t_diagramobjects, ordered by
581
+ # Sequence) followed by connector entries (t_diagramlinks).
582
+ # Derived from examples/exports/basic/model.xml:
583
+ # placement: geometry="Left=..;Top=..;Right=..;Bottom=..;
584
+ # imgL=..;imgT=..;imgR=..;imgB=..;" from the Rect* columns
585
+ # (Top/Bottom sign-flipped; img* at fixed +25/+30 offsets),
586
+ # seqno=Sequence, style=ObjectStyle verbatim.
587
+ # connector: subject is the CONNECTOR's EAID, geometry is the
588
+ # link's Geometry plus "Path=<path>;", style is the link's
589
+ # Style plus "Hidden=<hidden>;".
590
+ def diagram_elements_xml(dgm)
591
+ placements = @database.diagram_objects_for(dgm.diagram_id)
592
+ .sort_by { |placement| placement_order(placement) }
593
+ .filter_map { |placement| object_placement_xml(placement) }
594
+ links = @database.diagram_links_for(dgm.diagram_id)
595
+ .filter_map { |link| connector_entry_xml(link) }
596
+ inner = (placements + links).join("\n")
597
+ return "\t\t\t<elements/>" if inner.empty?
598
+
599
+ "\t\t\t<elements>\n#{inner}\n\t\t\t</elements>"
600
+ end
601
+
602
+ IMG_OFFSET_X = 25
603
+ IMG_OFFSET_Y = 30
604
+
605
+ def object_placement_xml(placement)
606
+ obj = @database.find_object(placement.ea_object_id)
607
+ return nil unless obj
608
+
609
+ "\t\t\t\t<element geometry=\"#{placement_geometry(placement)}\" " \
610
+ "subject=\"#{escape(@context.xmi_id_for(obj).to_s)}\" " \
611
+ "seqno=\"#{placement.sequence.to_i}\" style=\"#{escape(placement.objectstyle.to_s)}\"/>"
612
+ end
613
+
614
+ # Sequence is nullable with DEFAULT 0 — nil sorts as 0, ties
615
+ # broken stably by instance id.
616
+ def placement_order(placement)
617
+ [placement.sequence.to_i, placement.instance_id.to_i]
618
+ end
619
+
620
+ # Rect columns are nullable with DEFAULT 0 — coerce nil to 0.
621
+ def placement_geometry(placement)
622
+ left = placement.rectleft.to_i
623
+ top = -placement.recttop.to_i
624
+ right = placement.rectright.to_i
625
+ bottom = -placement.rectbottom.to_i
626
+ "Left=#{left};Top=#{top};Right=#{right};Bottom=#{bottom};" \
627
+ "imgL=#{left + IMG_OFFSET_X};imgT=#{top + IMG_OFFSET_Y};" \
628
+ "imgR=#{right + IMG_OFFSET_X};imgB=#{bottom + IMG_OFFSET_Y};"
629
+ end
630
+
631
+ def connector_entry_xml(link)
632
+ conn = @database.find_connector(link.connectorid)
633
+ return nil unless conn
634
+
635
+ # EA rewrites the Path waypoints' ";" separators to "$" and
636
+ # terminates with one ";" (reference: Path=165:-230$371:-230$;).
637
+ geometry = "#{style_fragment(link.geometry)}Path=#{link.path.to_s.tr(";", "$")};"
638
+ style = "#{style_fragment(link.style)}Hidden=#{link.hidden};"
639
+ "\t\t\t\t<element geometry=\"#{escape(geometry)}\" " \
640
+ "subject=\"#{escape(@context.xmi_id_for(conn).to_s)}\" style=\"#{escape(style)}\"/>"
641
+ end
642
+
643
+ # Real rows (plateau model) can lack the trailing ";" — append
644
+ # exactly one so concatenated tokens never fuse.
645
+ def style_fragment(text)
646
+ value = text.to_s
647
+ return "" if value.empty?
648
+
649
+ value.end_with?(";") ? value : "#{value};"
650
+ end
651
+
652
+ # ---- helpers ----------------------------------------------------
653
+
654
+ def parent_package_ref(pkg)
655
+ return nil unless pkg.parent_id
656
+
657
+ parent = @database.find_package(pkg.parent_id)
658
+ parent ? GuidFormat.ea_guid_to_xmi_id(parent.ea_guid, prefix: "EAPK") : nil
659
+ end
660
+
661
+ def owner_package_ref(obj)
662
+ return nil unless obj.package_id
663
+
664
+ pkg = @database.find_package(obj.package_id)
665
+ pkg ? GuidFormat.ea_guid_to_xmi_id(pkg.ea_guid, prefix: "EAPK") : nil
666
+ end
667
+
668
+ def package_guid_for(dgm)
669
+ pkg = @database.find_package(dgm.package_id)
670
+ pkg&.ea_guid || dgm.ea_guid
671
+ end
672
+
673
+ # Returns the documentation string for a record. Package notes
674
+ # and object notes come from different columns; both flow
675
+ # through here as the <properties documentation="..."> value.
676
+ def record_documentation(note)
677
+ note.nil? || note.empty? ? nil : note
678
+ end
679
+
680
+ def escape(text)
681
+ XmlEscape.call(text)
682
+ end
683
+ end
684
+ end
685
+ end
686
+ end