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
|
@@ -1,56 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# This file preserves backward compatibility for callers that
|
|
4
|
+
# reference `Ea::Transformers::QeaToXmi::GuidFormat`. The canonical
|
|
5
|
+
# implementation now lives at `Ea::GuidFormat` (shared across
|
|
6
|
+
# subsystems — DRY, MECE). See lib/ea/guid_format.rb.
|
|
3
7
|
module Ea
|
|
4
8
|
module Transformers
|
|
5
9
|
module QeaToXmi
|
|
6
|
-
|
|
7
|
-
# strings. No state, no I/O — same input always yields same output.
|
|
8
|
-
#
|
|
9
|
-
# EA stores identifiers as `{ABCD-1234-...}` braced GUID strings. XMI
|
|
10
|
-
# serializations use unbraced, dash→underscore forms prefixed with
|
|
11
|
-
# `EAID_` (most elements) or `EAPK_` (packages at the model root).
|
|
12
|
-
module GuidFormat
|
|
13
|
-
# Braces and dashes both become underscores; collapse runs of
|
|
14
|
-
# consecutive underscores so `{AB-CD}` → `AB_CD` (not `_AB_CD_`).
|
|
15
|
-
SEP = /[-{}]/
|
|
16
|
-
|
|
17
|
-
module_function
|
|
18
|
-
|
|
19
|
-
# @param ea_guid [String, nil] e.g. "{AB-CD-EF}"
|
|
20
|
-
# @param prefix [String] "EAID" (default) or "EAPK"
|
|
21
|
-
# @return [String, nil] e.g. "EAID_AB_CD_EF"
|
|
22
|
-
def ea_guid_to_xmi_id(ea_guid, prefix: "EAID")
|
|
23
|
-
return nil if ea_guid.nil? || ea_guid.empty?
|
|
24
|
-
|
|
25
|
-
clean = ea_guid.gsub(SEP, "_").gsub(/_+/, "_").gsub(/\A_/, "").gsub(/_\z/, "")
|
|
26
|
-
"#{prefix}_#{clean}"
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# @param xmi_id [String, nil] e.g. "EAID_AB_CD_EF"
|
|
30
|
-
# @return [String, nil] e.g. "{AB-CD-EF}"
|
|
31
|
-
def xmi_id_to_ea_guid(xmi_id)
|
|
32
|
-
return nil if xmi_id.nil? || xmi_id.empty?
|
|
33
|
-
|
|
34
|
-
body = xmi_id.sub(/\A(?:EAID|EAPK)_/, "")
|
|
35
|
-
"{#{body.tr('_', '-')}}"
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Build a member-end xmi:id for one side of a connector.
|
|
39
|
-
#
|
|
40
|
-
# Sparx EA's convention: take the connector's GUID, drop the first two
|
|
41
|
-
# hex characters of its first segment, and prepend `src` or `dst`.
|
|
42
|
-
#
|
|
43
|
-
# @param connector_xmi_id [String] e.g. "EAID_AB12CDEF_..."
|
|
44
|
-
# @param side [Symbol] :source or :destination
|
|
45
|
-
# @return [String] e.g. "EAID_src2CDEF_..."
|
|
46
|
-
def connector_end_xmi_id(connector_xmi_id, side:)
|
|
47
|
-
tag = side == :source ? "src" : "dst"
|
|
48
|
-
body = connector_xmi_id.sub(/\A(?:EAID|EAPK)_/, "")
|
|
49
|
-
first_segment, rest = body.split("_", 2)
|
|
50
|
-
trimmed = first_segment.length > 2 ? first_segment[2..] : first_segment
|
|
51
|
-
rest ? "EAID_#{tag}#{trimmed}_#{rest}" : "EAID_#{tag}#{trimmed}"
|
|
52
|
-
end
|
|
53
|
-
end
|
|
10
|
+
GuidFormat = ::Ea::GuidFormat
|
|
54
11
|
end
|
|
55
12
|
end
|
|
56
13
|
end
|
|
@@ -4,87 +4,88 @@ module Ea
|
|
|
4
4
|
module Transformers
|
|
5
5
|
module QeaToXmi
|
|
6
6
|
# Allocates synthetic xmi:id values for elements that don't have a
|
|
7
|
-
# natural GUID-based one —
|
|
8
|
-
#
|
|
7
|
+
# natural GUID-based one — literal `<lowerValue>`/`<upperValue>`
|
|
8
|
+
# bounds, instance slots and their values, return parameters.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
# `SL` (Slot), `NL` (NameLabel), `DB` (DiagramBounds), `OE` (OpaqueExpr),
|
|
12
|
-
# `RT` (Return parameter) for these synthesized identifiers.
|
|
10
|
+
# EA's scheme, derived from examples/exports/*/model.xml:
|
|
13
11
|
#
|
|
14
|
-
#
|
|
12
|
+
# EAID_<PREFIX><NNNNNN><SEP><TAIL27>
|
|
15
13
|
#
|
|
16
|
-
# -
|
|
17
|
-
# - `<
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
14
|
+
# - `<PREFIX>` is a Sparx-reserved literal prefix (LI, SL, OE, RT).
|
|
15
|
+
# - `<NNNNNN>` is a 6-digit zero-padded counter. LI counts GLOBALLY
|
|
16
|
+
# from 1 in allocation order (lower before upper, destination end
|
|
17
|
+
# before source end, document walk order across owners). SL, OE
|
|
18
|
+
# and RT count from 0 PER OWNING ELEMENT (slots/values per
|
|
19
|
+
# InstanceSpecification, return parameters per operation).
|
|
20
|
+
# - `<TAIL27>` is the owner ID's last 27 characters (the EA GUID
|
|
21
|
+
# minus its first segment).
|
|
22
|
+
# - `<SEP>` is as many underscores as keep the synthesized ID the
|
|
23
|
+
# same total width as the owner ID (one for 41-char EAID_ owners,
|
|
24
|
+
# two for 42-char src/dst association-end owners).
|
|
25
25
|
#
|
|
26
|
-
# The allocator
|
|
27
|
-
#
|
|
26
|
+
# The allocator memoizes by [owner_id, prefix, seed]: the same
|
|
27
|
+
# triple always returns the same ID without advancing any counter.
|
|
28
|
+
#
|
|
29
|
+
# @api private — internal to the QeaToXmi transformer; its
|
|
30
|
+
# signature follows the transformer's needs, not semver.
|
|
28
31
|
class IdAllocator
|
|
29
32
|
# Well-known prefixes Sparx uses for synthesized IDs.
|
|
30
33
|
LITERAL_INTEGER = "LI"
|
|
31
34
|
OPAQUE_EXPRESSION = "OE"
|
|
32
35
|
SLOT = "SL"
|
|
33
|
-
NAME_LABEL = "NL"
|
|
34
|
-
DIAGRAM_BOUNDS = "DB"
|
|
35
36
|
RETURN_PARAMETER = "RT"
|
|
36
37
|
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
|
|
38
|
+
# Prefixes whose counter runs across the whole document, starting
|
|
39
|
+
# at 1. All other prefixes restart at 0 per owner.
|
|
40
|
+
GLOBAL_PREFIXES = [LITERAL_INTEGER].freeze
|
|
41
|
+
|
|
42
|
+
TAIL_LENGTH = 27
|
|
43
|
+
# "EAID_" plus the 8-character prefix+counter token.
|
|
44
|
+
HEAD_LENGTH = 13
|
|
40
45
|
|
|
41
46
|
def initialize
|
|
42
|
-
@
|
|
47
|
+
@global_counters = Hash.new(0)
|
|
48
|
+
@owner_counters = Hash.new(0)
|
|
43
49
|
@assigned = {}
|
|
44
50
|
end
|
|
45
51
|
|
|
46
|
-
# @param prefix [String]
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
+
# @param prefix [String] LITERAL_INTEGER, SLOT, OPAQUE_EXPRESSION
|
|
53
|
+
# or RETURN_PARAMETER
|
|
54
|
+
# @param owner_id [String] the owning element's full xmi:id — its
|
|
55
|
+
# last 27 characters become the tail and its width sets the
|
|
56
|
+
# separator
|
|
57
|
+
# @param seed [String] stable memoization key within the owner
|
|
52
58
|
# @return [String] e.g. "EAID_LI000001__EEB1_4de7_98F5_670D6EE4A52B"
|
|
53
|
-
def allocate(prefix:, seed:
|
|
54
|
-
|
|
59
|
+
def allocate(prefix:, owner_id:, seed:)
|
|
60
|
+
key = [owner_id, prefix, seed]
|
|
61
|
+
return @assigned[key] if @assigned.key?(key)
|
|
55
62
|
|
|
56
|
-
@
|
|
57
|
-
id = compose_id(prefix: prefix, n: @counter, parent_guid: parent_guid)
|
|
58
|
-
@assigned[seed] = id if seed
|
|
59
|
-
id
|
|
63
|
+
@assigned[key] = compose_id(prefix, next_counter(prefix, owner_id), owner_id)
|
|
60
64
|
end
|
|
61
65
|
|
|
62
66
|
private
|
|
63
67
|
|
|
64
|
-
def
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# The format is `EAID_LI<NN>_<guid_tail>` where guid_tail
|
|
68
|
-
# preserves its leading underscore. Together with the
|
|
69
|
-
# separator `_` that yields the double-underscore form
|
|
70
|
-
# Sparx emits (`EAID_LI000001__EEB1_...`).
|
|
71
|
-
format("EAID_%<prefix>s%<n>06d_%<tail>s", prefix: prefix, n: n, tail: tail)
|
|
68
|
+
def next_counter(prefix, owner_id)
|
|
69
|
+
if GLOBAL_PREFIXES.include?(prefix)
|
|
70
|
+
@global_counters[prefix] += 1
|
|
72
71
|
else
|
|
73
|
-
|
|
72
|
+
n = @owner_counters[[prefix, owner_id]]
|
|
73
|
+
@owner_counters[[prefix, owner_id]] = n + 1
|
|
74
|
+
n
|
|
74
75
|
end
|
|
75
76
|
end
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return nil if parent_guid.nil? || parent_guid.empty?
|
|
78
|
+
def compose_id(prefix, counter, owner_id)
|
|
79
|
+
# QEA guid columns are nullable — an owner with no id (or one
|
|
80
|
+
# too short to carry a 27-char tail) gets a tailless,
|
|
81
|
+
# counter-only identifier.
|
|
82
|
+
return format("EAID_%<prefix>s%<n>06d", prefix: prefix, n: counter) if
|
|
83
|
+
owner_id.to_s.length < TAIL_LENGTH + HEAD_LENGTH
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
tail = owner_id[-TAIL_LENGTH..]
|
|
86
|
+
separator = "_" * (owner_id.length - TAIL_LENGTH - HEAD_LENGTH)
|
|
87
|
+
format("EAID_%<prefix>s%<n>06d%<sep>s%<tail>s",
|
|
88
|
+
prefix: prefix, n: counter, sep: separator, tail: tail)
|
|
88
89
|
end
|
|
89
90
|
end
|
|
90
91
|
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Transformers
|
|
5
|
+
module QeaToXmi
|
|
6
|
+
# Owns the `EAnone_` id shape and EA's <primitivetypes> Extension
|
|
7
|
+
# hierarchy: every distinct attribute/operation type name with a
|
|
8
|
+
# blank classifier becomes an `EAnone_<name>` PrimitiveType
|
|
9
|
+
# definition, in table first-use order (attributes then operations
|
|
10
|
+
# — verified to match the reference exports' ordering).
|
|
11
|
+
#
|
|
12
|
+
# `definition_id` is also what the transformer's type REFERENCES
|
|
13
|
+
# go through, so a reference and its definition normalize the name
|
|
14
|
+
# identically and cannot end up as two different ids.
|
|
15
|
+
#
|
|
16
|
+
# Names EA maps to OMG standard primitives are excluded on the
|
|
17
|
+
# attribute side only, and only when the classifier is blank, since
|
|
18
|
+
# that is the sole case where the attribute carries an href child
|
|
19
|
+
# instead of an idref.
|
|
20
|
+
#
|
|
21
|
+
# KNOWN GAP: a non-blank classifier that resolves to nothing (a
|
|
22
|
+
# numeric t_object id from a referenced model) still emits an
|
|
23
|
+
# `EAnone_` reference with no definition behind it. Defining those
|
|
24
|
+
# here over-emits instead: this scan walks every row in the
|
|
25
|
+
# database, including rows the package walk never exports, and EA's
|
|
26
|
+
# own plateau export defines nothing for them. Closing it means
|
|
27
|
+
# deriving definitions from the references the walk actually
|
|
28
|
+
# emitted, which is its own change.
|
|
29
|
+
module PrimitiveTypes
|
|
30
|
+
# EA primitive names that reference the OMG PrimitiveTypes
|
|
31
|
+
# library instead of a synthesized EAnone_ definition.
|
|
32
|
+
HREF_NAMES = { "int" => "Integer" }.freeze
|
|
33
|
+
|
|
34
|
+
OMG_PRIMITIVES = "http://www.omg.org/spec/UML/20110701/PrimitiveTypes.xmi"
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# @param database [Ea::Qea::Database]
|
|
39
|
+
# @return [Array<String>] distinct unresolved type names, first-use order
|
|
40
|
+
def unresolved_names(database)
|
|
41
|
+
(attribute_names(database) + operation_names(database)).uniq
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# An attribute reaches the OMG library through an href child only
|
|
45
|
+
# when it has no classifier to point at. One that names a
|
|
46
|
+
# classifier resolves against it instead, so it needs no
|
|
47
|
+
# definition here — see the KNOWN GAP above for the case where
|
|
48
|
+
# that classifier resolves to nothing.
|
|
49
|
+
def attribute_names(database)
|
|
50
|
+
database.attributes.filter_map do |attr|
|
|
51
|
+
next nil if href_for(attr.type, attr.classifier)
|
|
52
|
+
|
|
53
|
+
unresolved_name(attr.type, attr.classifier)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Operation returns have no href mechanism — they reference
|
|
58
|
+
# EAnone_<name> directly, so every name they use must be
|
|
59
|
+
# defined here or the idref dangles.
|
|
60
|
+
def operation_names(database)
|
|
61
|
+
database.operations.filter_map { |op| unresolved_name(op.type, op.classifier) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def unresolved_name(type, classifier)
|
|
65
|
+
name = normalize_name(type)
|
|
66
|
+
return nil if name.empty?
|
|
67
|
+
return nil unless blank_classifier?(classifier)
|
|
68
|
+
|
|
69
|
+
name
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The id EA gives a type name that has no classifier behind it.
|
|
73
|
+
# The reference and the definition both come from here, so they
|
|
74
|
+
# cannot normalize the name differently.
|
|
75
|
+
# @return [String] e.g. "EAnone_int"
|
|
76
|
+
def definition_id(type_name)
|
|
77
|
+
"EAnone_#{normalize_name(type_name)}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# EA type columns are free text; leading/trailing space is not
|
|
81
|
+
# part of the name.
|
|
82
|
+
def normalize_name(type)
|
|
83
|
+
type.to_s.strip
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The OMG href replaces the EAnone_ idref only when there is no
|
|
87
|
+
# classifier to point at. Nil means "not an href target", so the
|
|
88
|
+
# definition side and the transformer read one expression rather
|
|
89
|
+
# than asking a predicate and then fetching what it proved.
|
|
90
|
+
# @return [String, nil] OMG href for EA primitive names, else nil
|
|
91
|
+
def href_for(type, classifier)
|
|
92
|
+
return nil unless blank_classifier?(classifier)
|
|
93
|
+
|
|
94
|
+
target = HREF_NAMES[normalize_name(type)]
|
|
95
|
+
target && "#{OMG_PRIMITIVES}##{target}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def blank_classifier?(classifier)
|
|
99
|
+
text = normalize_name(classifier)
|
|
100
|
+
text.empty? || text == "0"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "xmi"
|
|
4
|
+
|
|
5
|
+
module Ea
|
|
6
|
+
module Transformers
|
|
7
|
+
module QeaToXmi
|
|
8
|
+
# Builds MDG technology stereotype definitions as xmi gem model
|
|
9
|
+
# objects for inclusion in the UML model tree.
|
|
10
|
+
#
|
|
11
|
+
# When EA exports a model that references MDG stereotypes, it
|
|
12
|
+
# includes the stereotype DEFINITIONS in the output — each as
|
|
13
|
+
# `<packagedElement xmi:type="uml:Stereotype">` with its tagged
|
|
14
|
+
# values as `<ownedAttribute>` elements, plus an
|
|
15
|
+
# `<uml:Extension>` linking the stereotype to its base metaclass.
|
|
16
|
+
#
|
|
17
|
+
# MDG technologies are registered at runtime via
|
|
18
|
+
# {Ea::Mdg::Registry} (OCP — swap in/out without code changes).
|
|
19
|
+
# The ProfileSerializer queries the registry at call time, so
|
|
20
|
+
# the output reflects whatever MDGs are currently registered.
|
|
21
|
+
#
|
|
22
|
+
# Uses the xmi gem's typed models (PackagedElement, OwnedAttribute,
|
|
23
|
+
# Type) for serialization — no raw XML string building.
|
|
24
|
+
class ProfileSerializer
|
|
25
|
+
UML_TYPE_HREF = "http://www.omg.org/spec/UML/20110701/UML.xmi"
|
|
26
|
+
|
|
27
|
+
# @param mdg_registry [Ea::Mdg::Registry, nil]
|
|
28
|
+
def initialize(mdg_registry)
|
|
29
|
+
@mdg_registry = mdg_registry
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Array<Xmi::Uml::PackagedElement>] stereotype and
|
|
33
|
+
# extension elements for all registered MDG documents.
|
|
34
|
+
# Empty array when no registry or no stereotypes.
|
|
35
|
+
#
|
|
36
|
+
# Stereotype ids are the bare names, so two MDGs defining the
|
|
37
|
+
# same name would emit clashing definitions. The first
|
|
38
|
+
# document to define a name wins.
|
|
39
|
+
def call
|
|
40
|
+
return [] unless @mdg_registry
|
|
41
|
+
|
|
42
|
+
@mdg_registry.documents
|
|
43
|
+
.flat_map(&:stereotypes)
|
|
44
|
+
.uniq(&:name)
|
|
45
|
+
.flat_map { |stereo| [build_stereotype(stereo), *build_extensions(stereo)] }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# @return [Xmi::Uml::PackagedElement] the uml:Stereotype element
|
|
51
|
+
def build_stereotype(stereotype)
|
|
52
|
+
::Xmi::Uml::PackagedElement.new(
|
|
53
|
+
type: "uml:Stereotype",
|
|
54
|
+
id: stereotype.name,
|
|
55
|
+
name: stereotype.name,
|
|
56
|
+
owned_attribute: owned_attributes_for(stereotype)
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [Array<Xmi::Uml::PackagedElement>] one uml:Extension
|
|
61
|
+
# per applies_to metaclass
|
|
62
|
+
def build_extensions(stereotype)
|
|
63
|
+
stereotype.applies_to.filter_map do |metaclass|
|
|
64
|
+
next nil if metaclass.nil? || metaclass.empty?
|
|
65
|
+
|
|
66
|
+
build_extension(stereotype, metaclass)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# EA carries both ends as one space-separated memberEnd
|
|
71
|
+
# attribute on the Extension and repeats it on the
|
|
72
|
+
# ExtensionEnd — never as child <memberEnd> elements.
|
|
73
|
+
def build_extension(stereotype, metaclass)
|
|
74
|
+
base_id = "#{stereotype.name}-base_#{metaclass}"
|
|
75
|
+
member_ends = "extension_#{stereotype.name} #{base_id}"
|
|
76
|
+
|
|
77
|
+
::Xmi::Uml::PackagedElement.new(
|
|
78
|
+
type: "uml:Extension",
|
|
79
|
+
id: "#{metaclass}_#{stereotype.name}",
|
|
80
|
+
name: "A_#{metaclass}_#{stereotype.name}",
|
|
81
|
+
member_end: member_ends,
|
|
82
|
+
owned_end: [build_extension_end(stereotype, member_ends)]
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_extension_end(stereotype, member_ends)
|
|
87
|
+
::Xmi::Uml::OwnedEnd.new(
|
|
88
|
+
type: "uml:ExtensionEnd",
|
|
89
|
+
id: "extension_#{stereotype.name}",
|
|
90
|
+
name: "extension_#{stereotype.name}",
|
|
91
|
+
member_end: member_ends,
|
|
92
|
+
is_composite: true,
|
|
93
|
+
uml_type: ::Xmi::Uml::Type.new(idref: stereotype.name)
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# All ownedAttribute elements: base_{metaclass} link(s) + one
|
|
98
|
+
# per tagged value.
|
|
99
|
+
def owned_attributes_for(stereotype)
|
|
100
|
+
attrs = base_properties(stereotype)
|
|
101
|
+
attrs.concat(tagged_value_properties(stereotype))
|
|
102
|
+
attrs
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @return [Array<Xmi::Uml::OwnedAttribute>] one base_{mc}
|
|
106
|
+
# property per applies_to metaclass, linking the stereotype
|
|
107
|
+
# to its UML base via the Extension association.
|
|
108
|
+
def base_properties(stereotype)
|
|
109
|
+
stereotype.applies_to.filter_map do |metaclass|
|
|
110
|
+
next nil if metaclass.nil? || metaclass.empty?
|
|
111
|
+
|
|
112
|
+
build_base_property(stereotype, metaclass)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def build_base_property(stereotype, metaclass)
|
|
117
|
+
::Xmi::Uml::OwnedAttribute.new(
|
|
118
|
+
type: "uml:Property",
|
|
119
|
+
id: "#{stereotype.name}-base_#{metaclass}",
|
|
120
|
+
name: "base_#{metaclass}",
|
|
121
|
+
association: "#{metaclass}_#{stereotype.name}",
|
|
122
|
+
uml_type: ::Xmi::Uml::Type.new(href: "#{UML_TYPE_HREF}##{metaclass}")
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @return [Array<Xmi::Uml::OwnedAttribute>] one per tagged value
|
|
127
|
+
def tagged_value_properties(stereotype)
|
|
128
|
+
stereotype.tagged_values.map do |tv|
|
|
129
|
+
build_tagged_value_property(stereotype.name, tv)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def build_tagged_value_property(stereo_name, tagged_value)
|
|
134
|
+
::Xmi::Uml::OwnedAttribute.new(
|
|
135
|
+
type: "uml:Property",
|
|
136
|
+
id: "#{stereo_name}-#{tagged_value.name}",
|
|
137
|
+
name: tagged_value.name,
|
|
138
|
+
uml_type: ::Xmi::Uml::Type.new(href: "#{UML_TYPE_HREF}##{normalize_type(tagged_value.type)}")
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Map MDG tagged value types to UML XMI primitive type names.
|
|
143
|
+
# Unknown types default to String (matches EA's behaviour).
|
|
144
|
+
def normalize_type(raw)
|
|
145
|
+
return "String" if raw.nil? || raw.empty?
|
|
146
|
+
|
|
147
|
+
TYPE_MAP.fetch(raw.to_s.downcase, "String")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
TYPE_MAP = {
|
|
151
|
+
"boolean" => "Boolean",
|
|
152
|
+
"integer" => "Integer",
|
|
153
|
+
"int" => "Integer",
|
|
154
|
+
"real" => "Real",
|
|
155
|
+
"double" => "Real",
|
|
156
|
+
"string" => "String",
|
|
157
|
+
"enumeration" => "String",
|
|
158
|
+
"class" => "Class"
|
|
159
|
+
}.freeze
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -20,8 +20,9 @@ module Ea
|
|
|
20
20
|
# `Variable` is the attribute name on the classifier (used to
|
|
21
21
|
# resolve the definingFeature EAID at transformation time).
|
|
22
22
|
# The `Value` plus `Op` form the OpaqueExpression body — Sparx
|
|
23
|
-
# prepends the operator
|
|
24
|
-
# `body="=Value"
|
|
23
|
+
# prepends the stored operator verbatim. Equality is stored as
|
|
24
|
+
# `Op==;` (parsed op "="), so `body="=Value"`; comparison ops
|
|
25
|
+
# keep their full text (`Op=<=;` → `body="<=Value"`).
|
|
25
26
|
#
|
|
26
27
|
# The parser is pure: no I/O, no state. Same input always yields
|
|
27
28
|
# the same output array of {Variable, Value, Op} structs.
|
|
@@ -32,15 +33,15 @@ module Ea
|
|
|
32
33
|
# @!attribute value [String] the literal value text
|
|
33
34
|
# @!attribute op [String] the operator character(s) EA stored
|
|
34
35
|
Binding = Struct.new(:variable, :value, :op) do
|
|
35
|
-
# Sparx serialises the value with the operator
|
|
36
|
-
# prepended (
|
|
37
|
-
#
|
|
36
|
+
# Sparx serialises the value with the stored operator
|
|
37
|
+
# prepended verbatim ("=" for equality, "<=" etc. as-is —
|
|
38
|
+
# EA's reference exports contain body="<=valueTwo").
|
|
38
39
|
#
|
|
39
40
|
# @return [String] the body to set on the OpaqueExpression
|
|
40
41
|
def body
|
|
41
42
|
return value if op.nil? || op.empty?
|
|
42
43
|
|
|
43
|
-
"#{op
|
|
44
|
+
"#{op}#{value}"
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
|