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
@@ -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
- # Pure conversions between Sparx EA GUID strings and XMI identifier
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 — e.g. literal `<lowerValue>` nodes, or
8
- # `<ownedComment>` bodies synthesized from EA's Note objects.
7
+ # natural GUID-based one — literal `<lowerValue>`/`<upperValue>`
8
+ # bounds, instance slots and their values, return parameters.
9
9
  #
10
- # Sparx's EAID format reserves prefixes like `LI` (LiteralInt),
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
- # Output shape: `EAID_<PREFIX><NNNNNN><GUID_TAIL>` where:
12
+ # EAID_<PREFIX><NNNNNN><SEP><TAIL27>
15
13
  #
16
- # - `EAID_` matches the prefix used by all other Sparx XMI element IDs.
17
- # - `<PREFIX>` is a Sparx-reserved literal prefix (LI, SL, OE, ...).
18
- # - `<NNNNNN>` is a 6-digit zero-padded counter, scoped to the
19
- # IdAllocator instance (one per `Transformer#serialize` call).
20
- # - `<GUID_TAIL>` is the parent element's EA GUID normalised to
21
- # Sparx's wire form. The leading underscore from the opening
22
- # brace of `{GUID-...}` is preserved so the output matches
23
- # real Sparx XMI byte-for-byte (e.g. `EAID_LI000001__EEB1_...`).
24
- # When `parent_guid` is nil, no tail is emitted.
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 is memoised by `seed`: same seed returns the same
27
- # allocated ID. Different seeds get different counters.
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
- # Leading-underscore-preserving normalisation: `{AB-CD}` `_AB_CD`.
38
- # Matches the wire form Sparx emits for parent-guid-suffixed IDs.
39
- GUID_BRACE_OR_DASH = /[-{}]/
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
- @counter = 0
47
+ @global_counters = Hash.new(0)
48
+ @owner_counters = Hash.new(0)
43
49
  @assigned = {}
44
50
  end
45
51
 
46
- # @param prefix [String] one of LITERAL_INTEGER, OPAQUE_EXPRESSION, ...
47
- # @param seed [String, nil] stable seed for memoization (e.g. the
48
- # source record's object_id). Same seed returns same allocated id.
49
- # @param parent_guid [String, nil] the owning element's EA GUID
50
- # (e.g. `{EEB1-...}`). When provided, the synthesised ID carries
51
- # the parent GUID tail so it is traceable and round-trip-safe.
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: nil, parent_guid: nil)
54
- return @assigned[seed] if seed && @assigned.key?(seed)
59
+ def allocate(prefix:, owner_id:, seed:)
60
+ key = [owner_id, prefix, seed]
61
+ return @assigned[key] if @assigned.key?(key)
55
62
 
56
- @counter += 1
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 compose_id(prefix:, n:, parent_guid:)
65
- tail = guid_tail(parent_guid)
66
- if tail
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
- format("EAID_%<prefix>s%<n>06d", prefix: prefix, n: n)
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
- # Sparx preserves the leading underscore that comes from the
78
- # opening brace of the EA GUID. Strip the closing-brace trailing
79
- # underscore only.
80
- #
81
- # `{EEB1-4de7-98F5-670D6EE4A52B}` `_EEB1_4de7_98F5_670D6EE4A52B`
82
- def guid_tail(parent_guid)
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
- parent_guid
86
- .gsub(GUID_BRACE_OR_DASH, "_")
87
- .sub(/_\z/, "")
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 character to the value (`Op==` →
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 character
36
- # prepended (`Op==` `body="=Value"`). For other operators
37
- # (`!=`, `<`, `>`) the full operator string is prepended.
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[0]}#{value}"
44
+ "#{op}#{value}"
44
45
  end
45
46
  end
46
47