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.
- checksums.yaml +4 -4
- data/TODO.complete/01-t-xref-parsing.md +1 -1
- data/TODO.complete/02-per-label-geometry.md +1 -1
- data/TODO.complete/03-object-properties-constraints.md +1 -1
- data/TODO.complete/04-operation-params.md +1 -1
- data/TODO.complete/05-pdata-flag-matrix.md +1 -1
- data/TODO.complete/06-emf-image-rendering.md +1 -1
- data/TODO.complete/07-auxiliary-tables.md +1 -1
- data/TODO.complete/10-mdg-stereotype-registry.md +1 -1
- data/TODO.complete/11-gml-xsd-generation.md +1 -1
- data/TODO.complete/13-shapescript-interpreter.md +12 -23
- data/TODO.complete/20-cli-diff.md +1 -1
- data/TODO.complete/21-cli-render.md +1 -1
- data/TODO.complete/22-cli-export.md +1 -1
- data/TODO.complete/23-cli-mdg.md +1 -1
- data/TODO.complete/30-headerlines-ocp-refactor.md +1 -1
- data/TODO.complete/32-spec-coverage.md +1 -1
- data/TODO.complete/33-blocked-parity-gaps.md +1 -1
- data/TODO.complete/36-stereotype-icon-wiring.md +1 -1
- data/TODO.complete/37-cli-lint.md +1 -1
- data/TODO.complete/38-cli-query.md +1 -1
- data/TODO.complete/39-cli-info.md +1 -1
- data/TODO.complete/40-ocl-evaluator.md +1 -1
- data/TODO.complete/41-round-trip-spec.md +1 -1
- data/TODO.complete/42-html-diff-report.md +1 -1
- data/TODO.complete/43-json-schema-export.md +1 -1
- data/TODO.complete/44-remaining-qea-tables.md +1 -1
- data/TODO.complete/45-shapescript-extensions.md +1 -1
- data/TODO.complete/46-xsd-consolidation.md +1 -1
- data/TODO.complete/47-headerlines-legacy-removal.md +1 -1
- data/TODO.complete/51-remove-respond-to.md +42 -15
- data/TODO.complete/52-xsd-decouple-fixtures.md +1 -1
- data/TODO.complete/54-wire-stereotype-icon-renderer.md +1 -1
- data/TODO.complete/55-wire-shapescript.md +1 -1
- data/TODO.complete/56-wire-emf-renderer.md +1 -1
- data/TODO.complete/57-wire-ocl-evaluator.md +1 -1
- data/TODO.complete/58-diff-modifications.md +1 -1
- data/TODO.complete/59-delete-toy-xmi-export.md +1 -1
- data/TODO.complete/60-curate-json-export.md +1 -1
- data/TODO.complete/61-plantuml-relationships.md +1 -1
- data/TODO.complete/62-changelog.md +1 -1
- data/TODO.complete/63-dependabot-fix.md +1 -1
- data/TODO.complete/64-qeatoxmi-connectors.md +9 -24
- data/TODO.complete/65-qeatoxmi-diagrams.md +9 -23
- data/TODO.complete/66-qeatoxmi-style-tags-docs.md +25 -18
- data/TODO.complete/67-ocl-collection-ops.md +12 -12
- data/TODO.complete/68-ocl-comparison-ops.md +16 -11
- data/TODO.complete/69-spec-new-models.md +13 -18
- data/TODO.complete/70-specs-for-all-models.md +21 -0
- data/TODO.complete/71-autoload-audit.md +26 -0
- data/TODO.complete/72-extension-serializer-ocp-naming.md +26 -0
- data/TODO.complete/73-ocl-validator-latent-bugs.md +50 -0
- data/TODO.complete/74-visibility-symbol-dry.md +39 -0
- data/TODO.complete/75-database-table-collection-registry.md +32 -0
- data/TODO.complete/76-stale-todo-cleanup.md +52 -0
- data/TODO.complete/77-eliminate-banned-patterns-from-specs.md +55 -0
- data/TODO.complete/78-mdg-stereotype-emission.md +66 -0
- data/TODO.complete/79-equivalence-fixes.md +57 -0
- data/TODO.complete/80-performance-extension-serializer-indexes.md +43 -0
- data/TODO.complete/81-dry-scope-guid-extraction.md +33 -0
- data/TODO.complete/82-public-send-audit-conclusion.md +25 -0
- data/TODO.complete/83-dry-shared-xml-escape-guid-format.md +52 -0
- data/TODO.complete/84-ocp-markers-shape-registry.md +54 -0
- data/TODO.complete/85-frozen-literal-rubocop-normalize-type-spec.md +38 -0
- data/TODO.complete/86-frozen-sweep-lint-query-specs.md +55 -0
- data/TODO.complete/STATUS.md +26 -5
- data/lib/ea/cli/app.rb +1 -0
- data/lib/ea/cli/command/export.rb +10 -1
- data/lib/ea/cli/command/mdg.rb +1 -10
- data/lib/ea/cli/command/render.rb +2 -1
- data/lib/ea/diff/html_reporter.rb +1 -5
- data/lib/ea/export/json/generator.rb +5 -6
- data/lib/ea/export/plantuml/generator.rb +5 -3
- data/lib/ea/guid_format.rb +91 -0
- data/lib/ea/mdg/document.rb +2 -1
- data/lib/ea/mdg/loader.rb +2 -1
- data/lib/ea/mdg/registry.rb +57 -2
- data/lib/ea/mdg/xml/applies_to.rb +1 -1
- data/lib/ea/mdg/xml/apply.rb +1 -1
- data/lib/ea/mdg/xml/content.rb +1 -1
- data/lib/ea/mdg/xml/documentation.rb +1 -1
- data/lib/ea/mdg/xml/stereotype.rb +1 -1
- data/lib/ea/mdg/xml/stereotypes.rb +1 -1
- data/lib/ea/mdg/xml/tag.rb +1 -1
- data/lib/ea/mdg/xml/tagged_values.rb +1 -1
- data/lib/ea/mdg/xml/technology.rb +1 -1
- data/lib/ea/mdg/xml/uml_profile.rb +1 -1
- data/lib/ea/mdg/xml/uml_profiles.rb +1 -1
- data/lib/ea/mdg/xml.rb +2 -1
- data/lib/ea/mdg.rb +2 -1
- data/lib/ea/model/ghost_label.rb +2 -1
- data/lib/ea/ocl/evaluator.rb +4 -2
- data/lib/ea/qea/database.rb +57 -2
- data/lib/ea/qea/factory/reference_resolver.rb +1 -1
- data/lib/ea/qea/models/ea_attribute.rb +1 -17
- data/lib/ea/qea/models/ea_connector.rb +3 -2
- data/lib/ea/qea/models/ea_object.rb +14 -9
- data/lib/ea/qea/models/ea_operation.rb +1 -17
- data/lib/ea/qea/models/ea_xref.rb +3 -0
- data/lib/ea/qea/models/scope_predicate.rb +29 -0
- data/lib/ea/qea/models.rb +1 -0
- data/lib/ea/qea/repositories/base_repository.rb +4 -1
- data/lib/ea/qea/validation/base_validator.rb +3 -48
- data/lib/ea/qea/validation/database/ocl_constraint_validator.rb +23 -13
- data/lib/ea/shapescript/renderer.rb +1 -3
- data/lib/ea/sources/qea/diagram_builder.rb +1 -0
- data/lib/ea/sources/qea/id_normalizer.rb +2 -2
- data/lib/ea/svg/ea_emitter/background.rb +2 -1
- data/lib/ea/svg/ea_emitter/element/attribute_line_builder.rb +1 -6
- data/lib/ea/svg/ea_emitter/element/operation_renderer.rb +2 -7
- data/lib/ea/svg/ea_emitter/elements.rb +1 -3
- data/lib/ea/svg/ea_emitter/ghost_labels.rb +2 -1
- data/lib/ea/svg/ea_emitter/label/end_label.rb +1 -6
- data/lib/ea/svg/ea_emitter/marker/package_import.rb +1 -1
- data/lib/ea/svg/ea_emitter/markers.rb +52 -23
- data/lib/ea/svg/ea_emitter/text_renderer.rb +1 -4
- data/lib/ea/svg/ea_emitter/visibility_symbol.rb +37 -0
- data/lib/ea/svg/ea_emitter.rb +1 -0
- data/lib/ea/svg/parity/source.rb +0 -2
- data/lib/ea/svg/parity/suite.rb +0 -1
- data/lib/ea/transformers/qea_to_xmi/cardinality.rb +40 -28
- data/lib/ea/transformers/qea_to_xmi/context.rb +35 -1
- data/lib/ea/transformers/qea_to_xmi/extension_serializer.rb +686 -0
- data/lib/ea/transformers/qea_to_xmi/guid_format.rb +5 -48
- data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +55 -54
- data/lib/ea/transformers/qea_to_xmi/primitive_types.rb +105 -0
- data/lib/ea/transformers/qea_to_xmi/profile_serializer.rb +163 -0
- data/lib/ea/transformers/qea_to_xmi/run_state.rb +7 -6
- data/lib/ea/transformers/qea_to_xmi/transformer.rb +396 -185
- data/lib/ea/transformers/qea_to_xmi/visibility.rb +30 -19
- data/lib/ea/transformers/qea_to_xmi.rb +5 -2
- data/lib/ea/transformers.rb +7 -2
- data/lib/ea/validation.rb +1 -1
- data/lib/ea/version.rb +1 -1
- data/lib/ea/xmi/liquid_drops/attribute_drop.rb +12 -4
- data/lib/ea/xmi/liquid_drops/klass_drop.rb +1 -2
- data/lib/ea/xmi/lookup_service.rb +4 -0
- data/lib/ea/xmi/parser.rb +30 -1
- data/lib/ea/xml_escape.rb +21 -0
- data/lib/ea.rb +2 -0
- metadata +28 -11
- data/lutaml-uml-0.5.1.gem +0 -0
|
@@ -34,6 +34,7 @@ module Ea
|
|
|
34
34
|
MODEL_NAME = "EA_Model"
|
|
35
35
|
EXPORTER = "Enterprise Architect"
|
|
36
36
|
EXPORTER_VERSION = "6.5"
|
|
37
|
+
BUILD_ID = "1624"
|
|
37
38
|
|
|
38
39
|
RELATIONSHIP_AT_PACKAGE_LEVEL = {
|
|
39
40
|
"Association" => :association,
|
|
@@ -55,12 +56,36 @@ module Ea
|
|
|
55
56
|
class: ->(obj) { build_class(obj) },
|
|
56
57
|
enumeration: ->(obj) { build_enumeration(obj) },
|
|
57
58
|
data_type: ->(obj) { build_data_type(obj) },
|
|
59
|
+
signal: ->(obj) { build_class(obj, xmi_type: "uml:Signal") },
|
|
58
60
|
instance: ->(obj) { build_instance(obj) },
|
|
61
|
+
association_element: ->(obj) { build_association_element(obj) },
|
|
59
62
|
}.freeze
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
# What each builder in CLASSIFIER_BUILDERS actually emits, keyed
|
|
65
|
+
# by the same symbols. Every builder must appear here: a kind
|
|
66
|
+
# left out silently loses its attribute bounds or its nested
|
|
67
|
+
# children — wrong XML, no exception — so the spec asserts these
|
|
68
|
+
# keys match CLASSIFIER_BUILDERS exactly.
|
|
69
|
+
#
|
|
70
|
+
# `:owned_attribute` — the builder emits `<ownedAttribute>` and so
|
|
71
|
+
# preallocates attribute bound IDs. Enumerations emit literals
|
|
72
|
+
# (never bounds) and instances emit slots.
|
|
73
|
+
# `:nested_classifier` — the builder walks children, so a kind
|
|
74
|
+
# listed here can adopt an object that carries a parentid.
|
|
75
|
+
# `build_class` is the only builder that walks children.
|
|
76
|
+
CLASSIFIER_CAPABILITIES = {
|
|
77
|
+
class: %i[owned_attribute nested_classifier],
|
|
78
|
+
signal: %i[owned_attribute nested_classifier],
|
|
79
|
+
data_type: %i[owned_attribute],
|
|
80
|
+
enumeration: [],
|
|
81
|
+
instance: [],
|
|
82
|
+
association_element: []
|
|
83
|
+
}.freeze
|
|
84
|
+
|
|
85
|
+
def initialize(database, mdg_registry: nil)
|
|
62
86
|
@database = database
|
|
63
|
-
@
|
|
87
|
+
@mdg_registry = mdg_registry
|
|
88
|
+
@context = Context.new(database: database)
|
|
64
89
|
end
|
|
65
90
|
|
|
66
91
|
# @return [String] XMI XML document
|
|
@@ -83,20 +108,68 @@ module Ea
|
|
|
83
108
|
# @return [String] XMI XML
|
|
84
109
|
def serialize(with_extensions: true)
|
|
85
110
|
xml = build_root.to_xml(use_prefix: true)
|
|
111
|
+
xml = normalize_namespaces(xml)
|
|
112
|
+
xml = plain_parameter_type(xml)
|
|
86
113
|
with_extensions ? inject_extension_content(xml) : xml
|
|
87
114
|
end
|
|
88
115
|
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
116
|
+
# EA writes a return parameter's type unprefixed:
|
|
117
|
+
# `<ownedParameter … type="EAnone_void"/>`. Its reference export
|
|
118
|
+
# carries 12 of these and no `xmi:type` on ownedParameter at all.
|
|
119
|
+
#
|
|
120
|
+
# `xmi:type` is the XMI metaclass discriminator; `type` is the
|
|
121
|
+
# classifier reference. Two different attributes — but
|
|
122
|
+
# lutaml-model matches by local name and collapses them into one
|
|
123
|
+
# slot, so the xmi gem can only emit one of them. It keeps the
|
|
124
|
+
# namespaced form, which is right for general UML XMI, and
|
|
125
|
+
# rewriting that in the shared model would break round-tripping
|
|
126
|
+
# for every non-Sparx consumer. Restoring the reference EA
|
|
127
|
+
# actually writes is this exporter's job, so it happens here.
|
|
128
|
+
#
|
|
129
|
+
# Scoped to ownedParameter: no other element wants it. Walks
|
|
130
|
+
# complete name="value" pairs rather than scanning the tag as
|
|
131
|
+
# text — a parameter name is free text out of EA and may itself
|
|
132
|
+
# contain the literal `xmi:type=`, which a raw substitution
|
|
133
|
+
# would happily rewrite inside the quotes.
|
|
134
|
+
ATTRIBUTE_PAIR = /([A-Za-z_][\w:.-]*)=("[^"]*")/
|
|
135
|
+
|
|
136
|
+
def plain_parameter_type(xml)
|
|
137
|
+
xml.gsub(/<ownedParameter\b[^>]*?>/m) do |tag|
|
|
138
|
+
tag.gsub(ATTRIBUTE_PAIR) do
|
|
139
|
+
name = Regexp.last_match(1)
|
|
140
|
+
name == "xmi:type" ? %(type=#{Regexp.last_match(2)}) : Regexp.last_match(0)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# EA's reference XMI uses the 2011-07-01 XMI/UML namespace URIs;
|
|
146
|
+
# the xmi gem's Root model hard-codes 2013-10-01. Post-process
|
|
147
|
+
# the serialized XML to match EA's namespace version. This is a
|
|
148
|
+
# targeted string replacement on the xmlns declarations only,
|
|
149
|
+
# not a semantic change.
|
|
150
|
+
def normalize_namespaces(xml)
|
|
151
|
+
xml.gsub("http://www.omg.org/spec/XMI/20131001",
|
|
152
|
+
"http://www.omg.org/spec/XMI/20110701")
|
|
153
|
+
.gsub("http://www.omg.org/spec/UML/20131001/UMLDI",
|
|
154
|
+
"http://www.omg.org/spec/UML/20110701/UMLDI")
|
|
155
|
+
.gsub("http://www.omg.org/spec/UML/20131001/UMLDC",
|
|
156
|
+
"http://www.omg.org/spec/UML/20110701/UMLDC")
|
|
157
|
+
.gsub("http://www.omg.org/spec/UML/20131001",
|
|
158
|
+
"http://www.omg.org/spec/UML/20110701")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Injects EA-specific extension content (elements, connectors,
|
|
162
|
+
# diagrams) into the serialized XMI. The xmi gem's Extension
|
|
163
|
+
# type is modeled but doesn't declare these children. We
|
|
164
|
+
# post-process the serialized XML to insert them inside the
|
|
165
|
+
# existing `<xmi:Extension>` element.
|
|
166
|
+
#
|
|
167
|
+
# Delegated to {ExtensionSerializer} — this class only handles
|
|
168
|
+
# the string substitution mechanics.
|
|
94
169
|
def inject_extension_content(xml)
|
|
95
|
-
content =
|
|
170
|
+
content = ExtensionSerializer.new(@database, @context).call
|
|
96
171
|
return xml if content.empty?
|
|
97
172
|
|
|
98
|
-
# Replace the empty `<xmi:Extension .../>` or the
|
|
99
|
-
# closing `</xmi:Extension>` with the populated version.
|
|
100
173
|
if xml.include?("<xmi:Extension")
|
|
101
174
|
xml.sub(%r{<xmi:Extension ([^>]+)>\s*</xmi:Extension>},
|
|
102
175
|
"<xmi:Extension \\1>\n#{content}\n\t</xmi:Extension>")
|
|
@@ -107,109 +180,6 @@ module Ea
|
|
|
107
180
|
end
|
|
108
181
|
end
|
|
109
182
|
|
|
110
|
-
# @return [String] XML for the inner content of <xmi:Extension>:
|
|
111
|
-
# <elements>...</elements>, <connectors>...</connectors>,
|
|
112
|
-
# <diagrams>...</diagrams>. Empty when none apply.
|
|
113
|
-
def extension_inner_xml
|
|
114
|
-
parts = []
|
|
115
|
-
connectors_xml = build_connectors_extension
|
|
116
|
-
parts << connectors_xml unless connectors_xml.empty?
|
|
117
|
-
diagrams_xml = build_diagrams_extension
|
|
118
|
-
parts << diagrams_xml unless diagrams_xml.empty?
|
|
119
|
-
parts.join("\n")
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def build_connectors_extension
|
|
123
|
-
connectors = @database.collections[:connectors] || []
|
|
124
|
-
return "" if connectors.empty?
|
|
125
|
-
|
|
126
|
-
inner = connectors.map { |conn| connector_xml(conn) }.compact.join("\n")
|
|
127
|
-
return "" if inner.empty?
|
|
128
|
-
|
|
129
|
-
"\t\t<connectors>\n#{inner}\n\t\t</connectors>"
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def build_diagrams_extension
|
|
133
|
-
diagrams = @database.collections[:diagrams] || []
|
|
134
|
-
return "" if diagrams.empty?
|
|
135
|
-
|
|
136
|
-
inner = diagrams.map { |dgm| diagram_xml(dgm) }.compact.join("\n")
|
|
137
|
-
return "" if inner.empty?
|
|
138
|
-
|
|
139
|
-
"\t\t<diagrams>\n#{inner}\n\t\t</diagrams>"
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Per-connector XML with xmi:idref to the model-level
|
|
143
|
-
# association (when present) plus source/target references.
|
|
144
|
-
def connector_xml(conn)
|
|
145
|
-
return nil unless conn.ea_guid
|
|
146
|
-
|
|
147
|
-
connector_id = "EAID_#{guid_to_xmi_id(conn.ea_guid)}"
|
|
148
|
-
source_obj = @database.collections[:objects]&.find { |o| o.ea_object_id == conn.start_object_id }
|
|
149
|
-
target_obj = @database.collections[:objects]&.find { |o| o.ea_object_id == conn.end_object_id }
|
|
150
|
-
return nil unless source_obj && target_obj
|
|
151
|
-
|
|
152
|
-
name_attr = conn.name ? %( name="#{escape(conn.name)}") : ""
|
|
153
|
-
inner = source_end_xml(conn, source_obj) + target_end_xml(conn, target_obj)
|
|
154
|
-
%(<connector xmi:idref="#{connector_id}"#{name_attr}>\n#{inner}\t\t</connector>)
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def source_end_xml(conn, source_obj)
|
|
158
|
-
end_xml(source_obj, "source")
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def target_end_xml(conn, target_obj)
|
|
162
|
-
end_xml(target_obj, "target")
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def end_xml(obj, kind)
|
|
166
|
-
xmi_id = "EAID_#{guid_to_xmi_id(obj.ea_guid)}"
|
|
167
|
-
lines = [
|
|
168
|
-
"\t\t\t<#{kind} xmi:idref=\"#{xmi_id}\">",
|
|
169
|
-
"\t\t\t\t<model ea_localid=\"#{obj.ea_object_id}\" " \
|
|
170
|
-
"type=\"#{obj.object_type}\" name=\"#{escape(obj.name.to_s)}\"/>",
|
|
171
|
-
"\t\t\t\t<role visibility=\"Public\" targetScope=\"instance\"/>",
|
|
172
|
-
"\t\t\t</#{kind}>"
|
|
173
|
-
]
|
|
174
|
-
lines.join("\n") + "\n"
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def diagram_xml(dgm)
|
|
178
|
-
return nil unless dgm.ea_guid
|
|
179
|
-
|
|
180
|
-
diagram_id = "EAID_#{guid_to_xmi_id(dgm.ea_guid)}"
|
|
181
|
-
pkg_guid = package_guid_for(dgm)
|
|
182
|
-
pkg_ref = pkg_guid ? "EAPK_#{guid_to_xmi_id(pkg_guid)}" : ""
|
|
183
|
-
lines = [
|
|
184
|
-
"<diagram xmi:id=\"#{diagram_id}\">",
|
|
185
|
-
"\t\t\t<model package=\"#{pkg_ref}\" localID=\"#{dgm.diagram_id}\" owner=\"#{pkg_ref}\"/>",
|
|
186
|
-
"\t\t\t<properties name=\"#{escape(dgm.name.to_s)}\" type=\"#{escape(dgm.diagram_type.to_s)}\"/>",
|
|
187
|
-
"\t\t</diagram>"
|
|
188
|
-
]
|
|
189
|
-
lines.join("\n")
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def package_guid_for(dgm)
|
|
193
|
-
pkg = (@database.collections[:packages] || []).find { |p| p.package_id == dgm.package_id }
|
|
194
|
-
pkg&.ea_guid || dgm.ea_guid
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def guid_to_xmi_id(guid)
|
|
198
|
-
return "" unless guid
|
|
199
|
-
|
|
200
|
-
guid.to_s.gsub(/[{}]/, "")
|
|
201
|
-
.tr("-", "_")
|
|
202
|
-
.prepend("EA") # truncated below
|
|
203
|
-
.sub(/\AEA/, "")
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
def escape(text)
|
|
207
|
-
text.to_s.gsub("&", "&")
|
|
208
|
-
.gsub("<", "<")
|
|
209
|
-
.gsub(">", ">")
|
|
210
|
-
.gsub('"', """)
|
|
211
|
-
end
|
|
212
|
-
|
|
213
183
|
private
|
|
214
184
|
|
|
215
185
|
# ---- Top-level construction --------------------------------------
|
|
@@ -226,6 +196,7 @@ module Ea
|
|
|
226
196
|
::Xmi::Documentation.new(
|
|
227
197
|
exporter: EXPORTER,
|
|
228
198
|
exporter_version: EXPORTER_VERSION,
|
|
199
|
+
exporter_id: BUILD_ID,
|
|
229
200
|
)
|
|
230
201
|
end
|
|
231
202
|
|
|
@@ -233,7 +204,7 @@ module Ea
|
|
|
233
204
|
::Xmi::Uml::UmlModel.new(
|
|
234
205
|
type: "uml:Model",
|
|
235
206
|
name: MODEL_NAME,
|
|
236
|
-
packaged_element: root_packages.map { |pkg| build_package(pkg) },
|
|
207
|
+
packaged_element: root_packages.map { |pkg| build_package(pkg) } + profile_elements,
|
|
237
208
|
)
|
|
238
209
|
end
|
|
239
210
|
|
|
@@ -247,6 +218,13 @@ module Ea
|
|
|
247
218
|
.sort_by { |p| [p.tpos || 0, p.name.to_s] }
|
|
248
219
|
end
|
|
249
220
|
|
|
221
|
+
# MDG stereotype definitions to include in the model tree.
|
|
222
|
+
# Empty when no MDG registry is wired. Each registered MDG
|
|
223
|
+
# contributes its stereotypes and extension relationships.
|
|
224
|
+
def profile_elements
|
|
225
|
+
ProfileSerializer.new(@mdg_registry).call
|
|
226
|
+
end
|
|
227
|
+
|
|
250
228
|
# ---- Package tree ------------------------------------------------
|
|
251
229
|
|
|
252
230
|
def build_package(pkg)
|
|
@@ -268,12 +246,128 @@ module Ea
|
|
|
268
246
|
end
|
|
269
247
|
|
|
270
248
|
def subpackages(pkg)
|
|
271
|
-
sorted_by_position(@context.child_packages(pkg.package_id))
|
|
272
|
-
|
|
249
|
+
@context.sorted_by_position(@context.child_packages(pkg.package_id))
|
|
250
|
+
.map { |sub| build_package(sub) }
|
|
273
251
|
end
|
|
274
252
|
|
|
253
|
+
# EA emits a package's own classifiers first, then the children
|
|
254
|
+
# nobody adopted. The global LI counter follows that order.
|
|
275
255
|
def classifier_objects(pkg)
|
|
276
|
-
|
|
256
|
+
top_level, orphaned = package_level_classifiers(pkg)
|
|
257
|
+
.partition { |obj| obj.parentid.to_i.zero? }
|
|
258
|
+
(top_level + orphaned).filter_map { |obj| build_classifier_with_bounds(obj) }
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Everything EA leaves at package level: an object stays here
|
|
262
|
+
# unless a classifier in the same package actually adopts it as
|
|
263
|
+
# a nestedClassifier. A child of an enumeration, of an
|
|
264
|
+
# instance, of a type we don't build, of a parent in another
|
|
265
|
+
# package, or of one that isn't there at all has no adopter.
|
|
266
|
+
#
|
|
267
|
+
# Adoption is decided from the package's own contents, never
|
|
268
|
+
# from what the walk has emitted so far — a child can sort
|
|
269
|
+
# before its own parent, and asking a set that the walk is
|
|
270
|
+
# still filling would emit that child twice.
|
|
271
|
+
def package_level_classifiers(pkg)
|
|
272
|
+
buildable_objects_in(pkg).reject { |obj| adopted?(obj) }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# A parent adopts a child only if it is a nesting kind in the
|
|
276
|
+
# same package AND the ParentID chain above the child actually
|
|
277
|
+
# terminates. A corrupt model can point an object at itself or
|
|
278
|
+
# round a cycle; leaving those adopted would drop them from the
|
|
279
|
+
# export, since the parent that owes them an emission is never
|
|
280
|
+
# reached. Shared with nested_classifiers_under so the two
|
|
281
|
+
# cannot disagree about who owns a child.
|
|
282
|
+
def adopted?(obj)
|
|
283
|
+
parent = nesting_parent_of(obj)
|
|
284
|
+
!parent.nil? && !cyclic_ancestry?(obj)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def nesting_parent_of(obj)
|
|
288
|
+
parent_id = obj.parentid.to_i
|
|
289
|
+
return nil unless parent_id.positive?
|
|
290
|
+
|
|
291
|
+
parent = @context.object_by_id(parent_id)
|
|
292
|
+
return nil unless parent && parent.package_id == obj.package_id
|
|
293
|
+
return nil unless nests_classifiers?(parent)
|
|
294
|
+
|
|
295
|
+
parent
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def cyclic_ancestry?(obj)
|
|
299
|
+
seen = Set[obj.ea_object_id]
|
|
300
|
+
current = obj
|
|
301
|
+
while (current = nesting_parent_of(current))
|
|
302
|
+
return true unless seen.add?(current.ea_object_id)
|
|
303
|
+
end
|
|
304
|
+
false
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# EA's class-centric LI allocation: attribute bounds first (name
|
|
308
|
+
# order), then the classifier itself, then the association-end
|
|
309
|
+
# bounds anchored here. The memoized allocator lets later
|
|
310
|
+
# builders reuse these IDs.
|
|
311
|
+
def build_classifier_with_bounds(obj)
|
|
312
|
+
preallocate_attribute_bounds(obj)
|
|
313
|
+
element = build_classifier(obj)
|
|
314
|
+
preallocate_end_bounds(obj)
|
|
315
|
+
element
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# EA allocates a classifier's attribute LI bounds in attribute
|
|
319
|
+
# NAME order, while the document emits attributes in Pos order.
|
|
320
|
+
# Pre-allocating here (memoized) makes the later Pos-ordered
|
|
321
|
+
# build reuse the name-ordered counters. Only owners that go on
|
|
322
|
+
# to emit ownedAttribute take counters — an enumeration's
|
|
323
|
+
# literals never carry bounds.
|
|
324
|
+
def preallocate_attribute_bounds(obj)
|
|
325
|
+
return unless emits_attributes?(obj)
|
|
326
|
+
|
|
327
|
+
@context.attributes_for(obj.ea_object_id)
|
|
328
|
+
.sort_by { |a| a.name.to_s }
|
|
329
|
+
.each { |attr| build_attribute_bounds(attr) }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Lower before upper — the global LI counter follows this order.
|
|
333
|
+
# @return [Array(Xmi::Uml::LowerValue, Xmi::Uml::UpperValue)]
|
|
334
|
+
def build_attribute_bounds(attr)
|
|
335
|
+
attr_id = @context.xmi_id_for(attr)
|
|
336
|
+
bounds = Cardinality.attribute_bounds(attr.lowerbound, attr.upperbound)
|
|
337
|
+
[build_lower_value(bounds[:lower],
|
|
338
|
+
seed: "mult-attr-#{attr.id}-lower", owner_id: attr_id),
|
|
339
|
+
build_upper_value(bounds[:upper],
|
|
340
|
+
seed: "mult-attr-#{attr.id}-upper", owner_id: attr_id)]
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# EA's global LI counter is allocated class-centrically, not in
|
|
344
|
+
# document order: after a classifier's attribute bounds come the
|
|
345
|
+
# association-end bounds whose OPPOSITE endpoint is that
|
|
346
|
+
# classifier (dst ends of connectors starting here, src ends of
|
|
347
|
+
# connectors ending here). The memoized allocator lets the
|
|
348
|
+
# Association element reuse these IDs when it is built later.
|
|
349
|
+
def preallocate_end_bounds(obj)
|
|
350
|
+
association_ends_anchored_at(obj)
|
|
351
|
+
.each { |conn, side| build_association_end(conn, side: side) }
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# The ends anchored at a classifier, ordered by connector GUID
|
|
355
|
+
# (ascending string order — the only key that fits every tied
|
|
356
|
+
# case in the reference exports; names, connector ids and
|
|
357
|
+
# opposite-endpoint ids all contradict at least one).
|
|
358
|
+
def association_ends_anchored_at(obj)
|
|
359
|
+
@context.connectors_for(obj.ea_object_id)
|
|
360
|
+
.flat_map { |conn| anchored_end_sides(conn, obj.ea_object_id) }
|
|
361
|
+
.sort_by { |conn, side| [conn.ea_guid.to_s, side == :destination ? 0 : 1] }
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def anchored_end_sides(conn, object_id)
|
|
365
|
+
return [] unless RELATIONSHIP_AT_PACKAGE_LEVEL[conn.connector_type] == :association
|
|
366
|
+
|
|
367
|
+
pairs = []
|
|
368
|
+
pairs << [conn, :destination] if conn.start_object_id == object_id
|
|
369
|
+
pairs << [conn, :source] if conn.end_object_id == object_id
|
|
370
|
+
pairs
|
|
277
371
|
end
|
|
278
372
|
|
|
279
373
|
# Connectors owned by this package: connectors whose start_object is
|
|
@@ -282,7 +376,7 @@ module Ea
|
|
|
282
376
|
# Realization are emitted inside the classifier itself.
|
|
283
377
|
def package_level_relationships(pkg)
|
|
284
378
|
emitted = Set.new
|
|
285
|
-
|
|
379
|
+
relationship_sources_in(pkg).flat_map do |obj|
|
|
286
380
|
@context.connectors_starting_at(obj.ea_object_id).filter_map do |conn|
|
|
287
381
|
key = RELATIONSHIP_AT_PACKAGE_LEVEL[conn.connector_type]
|
|
288
382
|
next nil unless key
|
|
@@ -293,6 +387,13 @@ module Ea
|
|
|
293
387
|
end
|
|
294
388
|
end
|
|
295
389
|
|
|
390
|
+
# Relationship discovery must also see Package objects —
|
|
391
|
+
# package-to-package Dependency connectors hang off them even
|
|
392
|
+
# though they never emit as classifiers.
|
|
393
|
+
def relationship_sources_in(pkg)
|
|
394
|
+
@context.objects_in_package(pkg.package_id).reject { |o| note?(o) }
|
|
395
|
+
end
|
|
396
|
+
|
|
296
397
|
def build_package_relationship(kind, conn)
|
|
297
398
|
case kind
|
|
298
399
|
when :association then build_association(conn)
|
|
@@ -307,16 +408,29 @@ module Ea
|
|
|
307
408
|
# ---- Classifier dispatch (OCP registry) --------------------------
|
|
308
409
|
|
|
309
410
|
def build_classifier(obj)
|
|
310
|
-
|
|
311
|
-
builder = CLASSIFIER_BUILDERS[kind]
|
|
411
|
+
builder = CLASSIFIER_BUILDERS[obj.transformer_type]
|
|
312
412
|
return nil unless builder
|
|
313
413
|
|
|
314
414
|
instance_exec(obj, &builder)
|
|
315
415
|
end
|
|
316
416
|
|
|
317
|
-
|
|
417
|
+
# Objects that build no classifier at all (a Component parent, a
|
|
418
|
+
# Note) reach these predicates too, and answer no to both.
|
|
419
|
+
def capabilities_of(obj)
|
|
420
|
+
CLASSIFIER_CAPABILITIES.fetch(obj.transformer_type, [])
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def emits_attributes?(obj)
|
|
424
|
+
capabilities_of(obj).include?(:owned_attribute)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def nests_classifiers?(obj)
|
|
428
|
+
capabilities_of(obj).include?(:nested_classifier)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def build_class(obj, xmi_type: nil)
|
|
318
432
|
::Xmi::Uml::PackagedElement.new(
|
|
319
|
-
type: class_xmi_type(obj),
|
|
433
|
+
type: xmi_type || class_xmi_type(obj),
|
|
320
434
|
id: @context.xmi_id_for(obj),
|
|
321
435
|
name: obj.name,
|
|
322
436
|
visibility: Visibility.from_scope(obj.scope),
|
|
@@ -325,9 +439,16 @@ module Ea
|
|
|
325
439
|
interface_realization: interface_realizations_for(obj),
|
|
326
440
|
owned_attribute: attributes_for(obj),
|
|
327
441
|
owned_operation: operations_for(obj),
|
|
442
|
+
nested_classifier: nested_children_for(obj),
|
|
328
443
|
)
|
|
329
444
|
end
|
|
330
445
|
|
|
446
|
+
# Recursive: nested children carry their own nested children,
|
|
447
|
+
# walked with the same class-centric bound preallocation.
|
|
448
|
+
def nested_children_for(obj)
|
|
449
|
+
nested_classifiers_under(obj).filter_map { |child| build_classifier_with_bounds(child) }
|
|
450
|
+
end
|
|
451
|
+
|
|
331
452
|
def build_enumeration(obj)
|
|
332
453
|
::Xmi::Uml::PackagedElement.new(
|
|
333
454
|
type: "uml:Enumeration",
|
|
@@ -360,6 +481,15 @@ module Ea
|
|
|
360
481
|
)
|
|
361
482
|
end
|
|
362
483
|
|
|
484
|
+
# EA stores some associations as t_object rows. Its export
|
|
485
|
+
# emits them bare — id and name only, no ownedEnds and no
|
|
486
|
+
# visibility. The connector-backed associations
|
|
487
|
+
# (build_association) are a separate path.
|
|
488
|
+
def build_association_element(obj)
|
|
489
|
+
::Xmi::Uml::PackagedElement.new(type: "uml:Association", id: @context.xmi_id_for(obj),
|
|
490
|
+
name: obj.name)
|
|
491
|
+
end
|
|
492
|
+
|
|
363
493
|
def build_comment(obj)
|
|
364
494
|
::Xmi::Uml::OwnedComment.new(
|
|
365
495
|
type: "uml:Comment",
|
|
@@ -381,28 +511,31 @@ module Ea
|
|
|
381
511
|
# rather than as package-level `<packagedElement type=
|
|
382
512
|
# "uml:Realization">`. We do the same.
|
|
383
513
|
def interface_realizations_for(obj)
|
|
384
|
-
realization_connectors(obj).
|
|
514
|
+
realization_connectors(obj).filter_map { |conn| build_interface_realization(conn) }
|
|
385
515
|
end
|
|
386
516
|
|
|
387
517
|
def attributes_for(obj)
|
|
388
|
-
sorted_by_position(@context.attributes_for(obj.ea_object_id))
|
|
389
|
-
|
|
518
|
+
@context.sorted_by_position(@context.attributes_for(obj.ea_object_id))
|
|
519
|
+
.map { |attr| build_attribute(attr) }
|
|
390
520
|
end
|
|
391
521
|
|
|
392
522
|
def operations_for(obj)
|
|
393
|
-
sorted_by_position(@context.operations_for(obj.ea_object_id))
|
|
394
|
-
|
|
523
|
+
@context.sorted_by_position(@context.operations_for(obj.ea_object_id))
|
|
524
|
+
.map { |op| build_operation(op) }
|
|
395
525
|
end
|
|
396
526
|
|
|
397
527
|
def enum_literals(obj)
|
|
398
|
-
sorted_by_position(@context.attributes_for(obj.ea_object_id))
|
|
399
|
-
|
|
528
|
+
@context.sorted_by_position(@context.attributes_for(obj.ea_object_id))
|
|
529
|
+
.map { |attr| build_owned_literal(attr) }
|
|
400
530
|
end
|
|
401
531
|
|
|
402
532
|
# ---- Leaf element builders --------------------------------------
|
|
403
533
|
|
|
404
534
|
def build_attribute(attr)
|
|
405
|
-
|
|
535
|
+
# An attribute always carries both bounds; association ends
|
|
536
|
+
# check their single card field instead and stay bare when it
|
|
537
|
+
# is blank.
|
|
538
|
+
lower, upper = build_attribute_bounds(attr)
|
|
406
539
|
::Xmi::Uml::OwnedAttribute.new(
|
|
407
540
|
type: "uml:Property",
|
|
408
541
|
id: @context.xmi_id_for(attr),
|
|
@@ -412,8 +545,8 @@ module Ea
|
|
|
412
545
|
is_ordered: Visibility.boolean_from_flag(attr.isordered),
|
|
413
546
|
is_derived: Visibility.boolean_from_flag(attr.derived),
|
|
414
547
|
uml_type: type_reference_model(attr.type, attr.classifier),
|
|
415
|
-
upper_value:
|
|
416
|
-
lower_value:
|
|
548
|
+
upper_value: upper,
|
|
549
|
+
lower_value: lower,
|
|
417
550
|
)
|
|
418
551
|
end
|
|
419
552
|
|
|
@@ -425,7 +558,7 @@ module Ea
|
|
|
425
558
|
is_static: Visibility.boolean_from_flag(op.isstatic),
|
|
426
559
|
is_abstract: Visibility.boolean_from_flag(op.abstract),
|
|
427
560
|
is_query: Visibility.boolean_from_flag(op.pure),
|
|
428
|
-
concurrency: op.concurrency
|
|
561
|
+
concurrency: normalize_concurrency(op.concurrency),
|
|
429
562
|
owned_parameter: operation_parameters(op),
|
|
430
563
|
)
|
|
431
564
|
end
|
|
@@ -450,30 +583,43 @@ module Ea
|
|
|
450
583
|
end
|
|
451
584
|
|
|
452
585
|
def build_interface_realization(conn)
|
|
586
|
+
client = @context.object_by_id(conn.start_object_id)
|
|
453
587
|
supplier = @context.object_by_id(conn.end_object_id)
|
|
588
|
+
return nil unless client && supplier
|
|
589
|
+
|
|
590
|
+
supplier_ref = @context.xmi_id_for(supplier)
|
|
454
591
|
::Xmi::Uml::InterfaceRealization.new(
|
|
455
|
-
type: "uml:InterfaceRealization",
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
client: @context.xmi_id_for(conn.start_object_id),
|
|
459
|
-
supplier: supplier ? @context.xmi_id_for(supplier) : nil,
|
|
460
|
-
contract: supplier ? @context.xmi_id_for(supplier) : nil,
|
|
592
|
+
type: "uml:InterfaceRealization", id: @context.xmi_id_for(conn),
|
|
593
|
+
name: conn.name, client: @context.xmi_id_for(client),
|
|
594
|
+
supplier: supplier_ref, contract: supplier_ref
|
|
461
595
|
)
|
|
462
596
|
end
|
|
463
597
|
|
|
464
598
|
def build_dependency(conn)
|
|
465
|
-
client =
|
|
466
|
-
supplier =
|
|
599
|
+
client = dependency_endpoint_ref(conn.start_object_id)
|
|
600
|
+
supplier = dependency_endpoint_ref(conn.end_object_id)
|
|
467
601
|
return nil unless client && supplier
|
|
468
602
|
|
|
469
603
|
::Xmi::Uml::PackagedElement.new(
|
|
470
604
|
type: "uml:Dependency",
|
|
471
605
|
id: @context.xmi_id_for(conn),
|
|
472
|
-
client:
|
|
473
|
-
supplier:
|
|
606
|
+
client: client,
|
|
607
|
+
supplier: supplier,
|
|
474
608
|
)
|
|
475
609
|
end
|
|
476
610
|
|
|
611
|
+
# Package-object endpoints resolve to their t_package via PDATA1
|
|
612
|
+
# and reference it as EAPK_ (matching EA); everything else keeps
|
|
613
|
+
# its own EAID_ reference.
|
|
614
|
+
def dependency_endpoint_ref(object_id)
|
|
615
|
+
obj = @context.object_by_id(object_id)
|
|
616
|
+
return nil unless obj
|
|
617
|
+
return @context.xmi_id_for(obj) unless package_object?(obj)
|
|
618
|
+
|
|
619
|
+
pkg = @database.find_package(obj.pdata1.to_i)
|
|
620
|
+
pkg && @context.xmi_id_for(pkg, prefix: "EAPK")
|
|
621
|
+
end
|
|
622
|
+
|
|
477
623
|
# Sparx serialisation order for `uml:Association` is
|
|
478
624
|
# destination member-end first, source member-end second.
|
|
479
625
|
# Reordering these breaks round-trip fidelity with Sparx EA
|
|
@@ -501,8 +647,18 @@ module Ea
|
|
|
501
647
|
target_id = side == :source ? conn.start_object_id : conn.end_object_id
|
|
502
648
|
target_obj = @context.object_by_id(target_id)
|
|
503
649
|
target_ref = target_obj ? @context.xmi_id_for(target_obj) : nil
|
|
504
|
-
bounds = Cardinality.parse(cardinality_for(conn, side))
|
|
505
650
|
containment = containment_for(conn, side)
|
|
651
|
+
# EA emits lower/upper only when the end's cardinality is set —
|
|
652
|
+
# a blank card gets a bare ownedEnd. Allocation order is lower
|
|
653
|
+
# before upper, with the END's id (EAID_dst…/EAID_src…) as the
|
|
654
|
+
# tail source.
|
|
655
|
+
raw_card = cardinality_for(conn, side)
|
|
656
|
+
lower = upper = nil
|
|
657
|
+
unless raw_card.to_s.strip.empty?
|
|
658
|
+
bounds = Cardinality.parse(raw_card)
|
|
659
|
+
lower = build_lower_value(bounds[:lower], seed: "mult-#{conn.connector_id}-#{side}-lower", owner_id: end_id)
|
|
660
|
+
upper = build_upper_value(bounds[:upper], seed: "mult-#{conn.connector_id}-#{side}-upper", owner_id: end_id)
|
|
661
|
+
end
|
|
506
662
|
|
|
507
663
|
model = ::Xmi::Uml::OwnedEnd.new(
|
|
508
664
|
type: "uml:Property",
|
|
@@ -512,8 +668,8 @@ module Ea
|
|
|
512
668
|
aggregation: Visibility.aggregation_from_containment(containment),
|
|
513
669
|
association: @context.xmi_id_for(conn),
|
|
514
670
|
uml_type: target_ref ? ::Xmi::Uml::Type.new(idref: target_ref) : nil,
|
|
515
|
-
upper_value:
|
|
516
|
-
lower_value:
|
|
671
|
+
upper_value: upper,
|
|
672
|
+
lower_value: lower,
|
|
517
673
|
)
|
|
518
674
|
|
|
519
675
|
AssociationEnd.new(end_id, model)
|
|
@@ -521,28 +677,28 @@ module Ea
|
|
|
521
677
|
|
|
522
678
|
# ---- Multiplicity helpers ---------------------------------------
|
|
523
679
|
|
|
524
|
-
#
|
|
525
|
-
#
|
|
526
|
-
#
|
|
527
|
-
def build_upper_value(raw, seed:,
|
|
680
|
+
# Callers emit bounds only when the EA cardinality field is set —
|
|
681
|
+
# EA's reference exports omit `<upperValue>`/`<lowerValue>`
|
|
682
|
+
# entirely on ends whose card is blank.
|
|
683
|
+
def build_upper_value(raw, seed:, owner_id:)
|
|
528
684
|
::Xmi::Uml::UpperValue.new(
|
|
529
685
|
type: "uml:LiteralUnlimitedNatural",
|
|
530
686
|
id: @context.id_allocator.allocate(
|
|
531
687
|
prefix: IdAllocator::LITERAL_INTEGER,
|
|
532
688
|
seed: seed,
|
|
533
|
-
|
|
689
|
+
owner_id: owner_id,
|
|
534
690
|
),
|
|
535
691
|
value: Cardinality.normalize_upper(raw),
|
|
536
692
|
)
|
|
537
693
|
end
|
|
538
694
|
|
|
539
|
-
def build_lower_value(raw, seed:,
|
|
695
|
+
def build_lower_value(raw, seed:, owner_id:)
|
|
540
696
|
::Xmi::Uml::LowerValue.new(
|
|
541
697
|
type: "uml:LiteralInteger",
|
|
542
698
|
id: @context.id_allocator.allocate(
|
|
543
699
|
prefix: IdAllocator::LITERAL_INTEGER,
|
|
544
700
|
seed: seed,
|
|
545
|
-
|
|
701
|
+
owner_id: owner_id,
|
|
546
702
|
),
|
|
547
703
|
value: Cardinality.normalize_lower(raw),
|
|
548
704
|
)
|
|
@@ -551,18 +707,31 @@ module Ea
|
|
|
551
707
|
# ---- Operation parameters ---------------------------------------
|
|
552
708
|
|
|
553
709
|
def operation_parameters(op)
|
|
554
|
-
params =
|
|
710
|
+
params = @context.params_for_operation(op.operationid)
|
|
555
711
|
.reject(&:return?)
|
|
556
712
|
.map { |p| build_owned_parameter(p) }
|
|
557
|
-
params << build_return_parameter(op) if op
|
|
713
|
+
params << build_return_parameter(op) if synthesized_return?(op)
|
|
558
714
|
params
|
|
559
715
|
end
|
|
560
716
|
|
|
717
|
+
# An untyped operation has no return to describe. One with no
|
|
718
|
+
# usable GUID has nothing to anchor the RT id's tail on, and the
|
|
719
|
+
# extension block skips it for the same reason — emitting it
|
|
720
|
+
# here would leave the two describing different operations.
|
|
721
|
+
# Type is blank-checked the way PrimitiveTypes normalizes names,
|
|
722
|
+
# so a whitespace-only Type cannot synthesize a return that
|
|
723
|
+
# primitive discovery then treats as absent.
|
|
724
|
+
def synthesized_return?(op)
|
|
725
|
+
return false if PrimitiveTypes.normalize_name(op.type).empty?
|
|
726
|
+
|
|
727
|
+
@context.identifiable?(op)
|
|
728
|
+
end
|
|
729
|
+
|
|
561
730
|
def build_owned_parameter(param)
|
|
562
731
|
::Xmi::Uml::OwnedParameter.new(
|
|
563
732
|
id: @context.xmi_id_for(param),
|
|
564
733
|
name: param.name,
|
|
565
|
-
direction: param.kind
|
|
734
|
+
direction: normalize_direction(param.kind),
|
|
566
735
|
)
|
|
567
736
|
end
|
|
568
737
|
|
|
@@ -571,10 +740,11 @@ module Ea
|
|
|
571
740
|
id: @context.id_allocator.allocate(
|
|
572
741
|
prefix: IdAllocator::RETURN_PARAMETER,
|
|
573
742
|
seed: "return-#{op.operationid}",
|
|
574
|
-
|
|
743
|
+
owner_id: @context.xmi_id_for(op),
|
|
575
744
|
),
|
|
576
745
|
name: "return",
|
|
577
746
|
direction: "return",
|
|
747
|
+
type: type_reference(op.type, op.classifier),
|
|
578
748
|
)
|
|
579
749
|
end
|
|
580
750
|
|
|
@@ -584,6 +754,26 @@ module Ea
|
|
|
584
754
|
obj.interface? ? "uml:Interface" : "uml:Class"
|
|
585
755
|
end
|
|
586
756
|
|
|
757
|
+
# EA's operation concurrency defaults to "Sequential" — omit
|
|
758
|
+
# it so the xmi gem doesn't emit concurrency="sequential"
|
|
759
|
+
# (EA's reference XMI omits the default).
|
|
760
|
+
def normalize_concurrency(raw)
|
|
761
|
+
return nil if raw.nil? || raw.to_s.strip.empty?
|
|
762
|
+
|
|
763
|
+
value = raw.to_s.downcase
|
|
764
|
+
value == "sequential" ? nil : value
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# UML parameter direction defaults to "in" — omit it so the
|
|
768
|
+
# xmi gem doesn't emit direction="in" (EA's reference XMI
|
|
769
|
+
# omits the default).
|
|
770
|
+
def normalize_direction(raw)
|
|
771
|
+
return nil if raw.nil? || raw.to_s.strip.empty?
|
|
772
|
+
|
|
773
|
+
value = raw.to_s.downcase
|
|
774
|
+
value == "in" ? nil : value
|
|
775
|
+
end
|
|
776
|
+
|
|
587
777
|
def inheritance_connectors(obj, type)
|
|
588
778
|
@context.connectors_for(obj.ea_object_id).select do |conn|
|
|
589
779
|
conn.start_object_id == obj.ea_object_id && conn.connector_type == type
|
|
@@ -591,15 +781,32 @@ module Ea
|
|
|
591
781
|
end
|
|
592
782
|
|
|
593
783
|
# Realization connectors owned by this class — those where
|
|
594
|
-
# this object is the source (client)
|
|
595
|
-
#
|
|
784
|
+
# this object is the source (client). Each emits an
|
|
785
|
+
# `<interfaceRealization>`. Matched via the model predicate so
|
|
786
|
+
# both EA spellings (Realization/Realisation) are recognized.
|
|
596
787
|
def realization_connectors(obj)
|
|
597
|
-
|
|
788
|
+
@context.connectors_for(obj.ea_object_id).select do |conn|
|
|
789
|
+
conn.start_object_id == obj.ea_object_id && conn.realization?
|
|
790
|
+
end
|
|
598
791
|
end
|
|
599
792
|
|
|
600
|
-
|
|
793
|
+
# EA walks classifiers in tree-position order (t_object.TPos,
|
|
794
|
+
# ties broken by name then object id), not insertion order.
|
|
795
|
+
# The global LI counter depends on it.
|
|
796
|
+
def buildable_objects_in(pkg)
|
|
601
797
|
@context.objects_in_package(pkg.package_id)
|
|
602
798
|
.reject { |o| note?(o) || package_object?(o) }
|
|
799
|
+
.sort_by { |o| [o.sort_position, o.name.to_s, o.ea_object_id] }
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
# Children nested under a classifier via t_object.ParentID.
|
|
803
|
+
# Level-1 order in EA's reference is descending object id;
|
|
804
|
+
# deeper levels don't match any column we've found — the LI
|
|
805
|
+
# counters inside deep nests may drift until that's derived.
|
|
806
|
+
def nested_classifiers_under(obj)
|
|
807
|
+
@context.objects_in_package(obj.package_id)
|
|
808
|
+
.select { |o| o.parentid.to_i == obj.ea_object_id && adopted?(o) }
|
|
809
|
+
.sort_by { |o| [o.sort_position, -o.ea_object_id] }
|
|
603
810
|
end
|
|
604
811
|
|
|
605
812
|
def notes_in(pkg)
|
|
@@ -614,27 +821,39 @@ module Ea
|
|
|
614
821
|
obj.object_type == "Package"
|
|
615
822
|
end
|
|
616
823
|
|
|
617
|
-
def sorted_by_position(records)
|
|
618
|
-
records.sort_by { |r| [r.sort_position, r.name.to_s] }
|
|
619
|
-
end
|
|
620
|
-
|
|
621
824
|
def primitive?(obj)
|
|
622
825
|
obj.object_type == "PrimitiveType" ||
|
|
623
826
|
(obj.gentype == "Java" && obj.stereotype_is?("primitive"))
|
|
624
827
|
end
|
|
625
828
|
|
|
626
|
-
|
|
627
|
-
|
|
829
|
+
# The fallback id comes from the same module that emits the
|
|
830
|
+
# <primitivetypes> definitions, so the two normalize the name
|
|
831
|
+
# identically. A name that is blank once stripped gets no
|
|
832
|
+
# reference at all — PrimitiveTypes defines nothing for it.
|
|
833
|
+
def type_reference(type_name, classifier)
|
|
834
|
+
return nil if PrimitiveTypes.normalize_name(type_name).empty?
|
|
628
835
|
|
|
629
|
-
|
|
630
|
-
GuidFormat.ea_guid_to_xmi_id(classifier_guid)
|
|
631
|
-
else
|
|
632
|
-
"EAnone_#{type_name}"
|
|
633
|
-
end
|
|
836
|
+
classifier_ref(classifier) || PrimitiveTypes.definition_id(type_name)
|
|
634
837
|
end
|
|
635
838
|
|
|
636
|
-
|
|
637
|
-
|
|
839
|
+
# QEA stores t_attribute.Classifier / t_operation.Classifier as
|
|
840
|
+
# an INTEGER t_object id ("0" = unresolved); EAP-era data can
|
|
841
|
+
# carry a GUID instead. Resolve either to the object's xmi id.
|
|
842
|
+
def classifier_ref(classifier)
|
|
843
|
+
text = PrimitiveTypes.normalize_name(classifier)
|
|
844
|
+
return nil if PrimitiveTypes.blank_classifier?(text)
|
|
845
|
+
return GuidFormat.ea_guid_to_xmi_id(text) unless text.match?(/\A\d+\z/)
|
|
846
|
+
|
|
847
|
+
obj = @context.object_by_id(text.to_i)
|
|
848
|
+
obj && @context.xmi_id_for(obj)
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
def type_reference_model(type_name, classifier)
|
|
852
|
+
if (href = PrimitiveTypes.href_for(type_name, classifier))
|
|
853
|
+
return ::Xmi::Uml::Type.new(href: href)
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
ref = type_reference(type_name, classifier)
|
|
638
857
|
ref ? ::Xmi::Uml::Type.new(idref: ref) : nil
|
|
639
858
|
end
|
|
640
859
|
|
|
@@ -647,7 +866,7 @@ module Ea
|
|
|
647
866
|
end
|
|
648
867
|
|
|
649
868
|
def containment_for(conn, side)
|
|
650
|
-
side == :source ? conn.
|
|
869
|
+
side == :source ? conn.sourceisaggregate : conn.destisaggregate
|
|
651
870
|
end
|
|
652
871
|
|
|
653
872
|
# EA's t_connector does not expose per-end visibility (the
|
|
@@ -658,14 +877,6 @@ module Ea
|
|
|
658
877
|
nil
|
|
659
878
|
end
|
|
660
879
|
|
|
661
|
-
# The owning element for an attribute's synthesised IDs is the
|
|
662
|
-
# attribute's classifier (parent object), not the attribute
|
|
663
|
-
# itself — Sparx encodes the parent class GUID in the suffix.
|
|
664
|
-
def parent_guid_for_attribute(attr)
|
|
665
|
-
parent = @context.object_by_id(attr.ea_object_id)
|
|
666
|
-
parent&.ea_guid
|
|
667
|
-
end
|
|
668
|
-
|
|
669
880
|
# InstanceSpecification classifier reference. EA stores this
|
|
670
881
|
# directly in t_object.classifier as the classifier's
|
|
671
882
|
# ea_object_id (NOT pdata1, which is a separate column used
|
|
@@ -710,7 +921,7 @@ module Ea
|
|
|
710
921
|
@context.id_allocator.allocate(
|
|
711
922
|
prefix: IdAllocator::SLOT,
|
|
712
923
|
seed: "slot-#{instance.ea_object_id}-#{binding.variable}",
|
|
713
|
-
|
|
924
|
+
owner_id: @context.xmi_id_for(instance),
|
|
714
925
|
)
|
|
715
926
|
end
|
|
716
927
|
|
|
@@ -718,7 +929,7 @@ module Ea
|
|
|
718
929
|
@context.id_allocator.allocate(
|
|
719
930
|
prefix: IdAllocator::OPAQUE_EXPRESSION,
|
|
720
931
|
seed: "oe-#{instance.ea_object_id}-#{binding.variable}",
|
|
721
|
-
|
|
932
|
+
owner_id: @context.xmi_id_for(instance),
|
|
722
933
|
)
|
|
723
934
|
end
|
|
724
935
|
|