ea 0.5.1 → 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 (145) 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 +15 -0
  44. data/TODO.complete/65-qeatoxmi-diagrams.md +15 -0
  45. data/TODO.complete/66-qeatoxmi-style-tags-docs.md +35 -0
  46. data/TODO.complete/67-ocl-collection-ops.md +20 -0
  47. data/TODO.complete/68-ocl-comparison-ops.md +24 -0
  48. data/TODO.complete/69-spec-new-models.md +20 -0
  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/convert.rb +1 -1
  69. data/lib/ea/cli/command/export.rb +10 -1
  70. data/lib/ea/cli/command/mdg.rb +1 -10
  71. data/lib/ea/cli/command/render.rb +2 -1
  72. data/lib/ea/diff/html_reporter.rb +1 -5
  73. data/lib/ea/export/json/generator.rb +5 -6
  74. data/lib/ea/export/plantuml/generator.rb +5 -3
  75. data/lib/ea/guid_format.rb +91 -0
  76. data/lib/ea/mdg/document.rb +2 -1
  77. data/lib/ea/mdg/loader.rb +2 -1
  78. data/lib/ea/mdg/registry.rb +57 -2
  79. data/lib/ea/mdg/xml/applies_to.rb +1 -1
  80. data/lib/ea/mdg/xml/apply.rb +1 -1
  81. data/lib/ea/mdg/xml/content.rb +1 -1
  82. data/lib/ea/mdg/xml/documentation.rb +1 -1
  83. data/lib/ea/mdg/xml/stereotype.rb +1 -1
  84. data/lib/ea/mdg/xml/stereotypes.rb +1 -1
  85. data/lib/ea/mdg/xml/tag.rb +1 -1
  86. data/lib/ea/mdg/xml/tagged_values.rb +1 -1
  87. data/lib/ea/mdg/xml/technology.rb +1 -1
  88. data/lib/ea/mdg/xml/uml_profile.rb +1 -1
  89. data/lib/ea/mdg/xml/uml_profiles.rb +1 -1
  90. data/lib/ea/mdg/xml.rb +2 -1
  91. data/lib/ea/mdg.rb +2 -1
  92. data/lib/ea/model/ghost_label.rb +2 -1
  93. data/lib/ea/ocl/evaluator.rb +39 -0
  94. data/lib/ea/ocl/nodes.rb +9 -0
  95. data/lib/ea/ocl/parser.rb +60 -6
  96. data/lib/ea/qea/database.rb +57 -2
  97. data/lib/ea/qea/factory/reference_resolver.rb +1 -1
  98. data/lib/ea/qea/models/ea_attribute.rb +1 -17
  99. data/lib/ea/qea/models/ea_connector.rb +3 -2
  100. data/lib/ea/qea/models/ea_object.rb +14 -9
  101. data/lib/ea/qea/models/ea_operation.rb +1 -17
  102. data/lib/ea/qea/models/ea_xref.rb +3 -0
  103. data/lib/ea/qea/models/scope_predicate.rb +29 -0
  104. data/lib/ea/qea/models.rb +1 -0
  105. data/lib/ea/qea/repositories/base_repository.rb +4 -1
  106. data/lib/ea/qea/validation/base_validator.rb +3 -48
  107. data/lib/ea/qea/validation/database/ocl_constraint_validator.rb +23 -13
  108. data/lib/ea/shapescript/renderer.rb +1 -3
  109. data/lib/ea/sources/qea/diagram_builder.rb +1 -0
  110. data/lib/ea/sources/qea/id_normalizer.rb +2 -2
  111. data/lib/ea/svg/ea_emitter/background.rb +2 -1
  112. data/lib/ea/svg/ea_emitter/element/attribute_line_builder.rb +1 -6
  113. data/lib/ea/svg/ea_emitter/element/operation_renderer.rb +2 -7
  114. data/lib/ea/svg/ea_emitter/elements.rb +1 -3
  115. data/lib/ea/svg/ea_emitter/ghost_labels.rb +2 -1
  116. data/lib/ea/svg/ea_emitter/label/end_label.rb +1 -6
  117. data/lib/ea/svg/ea_emitter/marker/package_import.rb +1 -1
  118. data/lib/ea/svg/ea_emitter/markers.rb +52 -23
  119. data/lib/ea/svg/ea_emitter/text_renderer.rb +1 -4
  120. data/lib/ea/svg/ea_emitter/visibility_symbol.rb +37 -0
  121. data/lib/ea/svg/ea_emitter.rb +1 -0
  122. data/lib/ea/svg/parity/source.rb +0 -2
  123. data/lib/ea/svg/parity/suite.rb +0 -1
  124. data/lib/ea/transformers/qea_to_xmi/cardinality.rb +40 -28
  125. data/lib/ea/transformers/qea_to_xmi/context.rb +35 -1
  126. data/lib/ea/transformers/qea_to_xmi/extension_serializer.rb +686 -0
  127. data/lib/ea/transformers/qea_to_xmi/guid_format.rb +5 -48
  128. data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +55 -54
  129. data/lib/ea/transformers/qea_to_xmi/primitive_types.rb +105 -0
  130. data/lib/ea/transformers/qea_to_xmi/profile_serializer.rb +163 -0
  131. data/lib/ea/transformers/qea_to_xmi/run_state.rb +7 -6
  132. data/lib/ea/transformers/qea_to_xmi/transformer.rb +423 -76
  133. data/lib/ea/transformers/qea_to_xmi/visibility.rb +30 -19
  134. data/lib/ea/transformers/qea_to_xmi.rb +5 -2
  135. data/lib/ea/transformers.rb +8 -2
  136. data/lib/ea/validation.rb +1 -1
  137. data/lib/ea/version.rb +1 -1
  138. data/lib/ea/xmi/liquid_drops/attribute_drop.rb +12 -4
  139. data/lib/ea/xmi/liquid_drops/klass_drop.rb +1 -2
  140. data/lib/ea/xmi/lookup_service.rb +4 -0
  141. data/lib/ea/xmi/parser.rb +30 -1
  142. data/lib/ea/xml_escape.rb +21 -0
  143. data/lib/ea.rb +2 -0
  144. metadata +34 -11
  145. data/lutaml-uml-0.5.1.gem +0 -0
@@ -62,7 +62,7 @@ module Ea
62
62
  # Backwards-compatible API: count of marker bodies emitted.
63
63
  def count_for(connector)
64
64
  return 0 if connector.hidden
65
- return 0 unless connector.waypoints&.any? { |w| w.position }
65
+ return 0 unless connector.waypoints&.any?(&:position)
66
66
 
67
67
  entries_for(connector).size
68
68
  end
@@ -85,15 +85,51 @@ module Ea
85
85
  end
86
86
  end
87
87
 
88
+ # OCP registries for marker shapes. Adding a new marker shape
89
+ # = adding one entry to SHAPE_REGISTRY, not editing 3 case/when
90
+ # branches. Each entry carries the style_key and a render
91
+ # lambda that knows how to draw the shape.
92
+ #
93
+ # Style keys (:diamond_filled, :triangle_open, :connector_line)
94
+ # map to CSS strings via STYLE_MAP below.
95
+ SHAPE_REGISTRY = {
96
+ diamond: {
97
+ style_key: :diamond_filled,
98
+ render: ->(m, anchor, base) { m.diamond_polygon(anchor, base) }
99
+ },
100
+ triangle: {
101
+ style_key: :triangle_open,
102
+ render: ->(m, anchor, base) { m.triangle_polygon(anchor, base) }
103
+ },
104
+ plus: {
105
+ style_key: :connector_line,
106
+ render: ->(m, anchor, _base) { m.plus_path(anchor) }
107
+ },
108
+ arrow: {
109
+ style_key: :connector_line,
110
+ render: ->(m, anchor, base) { m.arrow_path(anchor, base) }
111
+ },
112
+ package_anchor: {
113
+ style_key: :connector_line,
114
+ render: ->(m, anchor, base) { m.package_anchor_path(anchor, base) }
115
+ }
116
+ }.freeze
117
+
118
+ # Style key → (fill, opacity) pair. The common stroke prefix
119
+ # is shared, so only the fill and opacity vary.
120
+ STYLE_MAP = {
121
+ diamond_filled: { fill: "#000000", opacity: "1.00" },
122
+ triangle_open: { fill: "#FFFFFF", opacity: "1.00" },
123
+ connector_line: { fill: "#000000", opacity: "0.00" }
124
+ }.freeze
125
+
88
126
  def style_for(key)
89
- case key
90
- when :diamond_filled
91
- "stroke-width:#{stroke_width};stroke-linecap:round;stroke-linejoin:bevel; fill:#000000;fill-opacity:1.00; stroke:#000000; stroke-opacity:1.00"
92
- when :triangle_open
93
- "stroke-width:#{stroke_width};stroke-linecap:round;stroke-linejoin:bevel; fill:#FFFFFF;fill-opacity:1.00; stroke:#000000; stroke-opacity:1.00"
94
- when :connector_line
95
- "stroke-width:#{stroke_width};stroke-linecap:round;stroke-linejoin:bevel; fill:#000000;fill-opacity:0.00; stroke:#000000; stroke-opacity:1.00"
96
- end
127
+ spec = STYLE_MAP[key]
128
+ return nil unless spec
129
+
130
+ "stroke-width:#{stroke_width};stroke-linecap:round;stroke-linejoin:bevel; " \
131
+ "fill:#{spec[:fill]};fill-opacity:#{spec[:opacity]}; " \
132
+ "stroke:#000000; stroke-opacity:1.00"
97
133
  end
98
134
 
99
135
  def visible_connectors
@@ -210,13 +246,7 @@ module Ea
210
246
  end
211
247
 
212
248
  def style_key_for(spec)
213
- case spec.shape
214
- when :diamond then :diamond_filled
215
- when :triangle then :triangle_open
216
- when :plus then :connector_line # shares style with line <g>
217
- when :arrow then :connector_line # share with line <g>
218
- when :package_anchor then :connector_line
219
- end
249
+ SHAPE_REGISTRY.dig(spec.shape, :style_key)
220
250
  end
221
251
 
222
252
  # Rounded [x, y] of the marker's tip (anchor) in canvas
@@ -227,13 +257,10 @@ module Ea
227
257
  end
228
258
 
229
259
  def render_shape(spec)
230
- case spec.shape
231
- when :diamond then diamond_polygon(spec.anchor, spec.base)
232
- when :triangle then triangle_polygon(spec.anchor, spec.base)
233
- when :plus then plus_path(spec.anchor)
234
- when :arrow then arrow_path(spec.anchor, spec.base)
235
- when :package_anchor then package_anchor_path(spec.anchor, spec.base)
236
- end
260
+ entry = SHAPE_REGISTRY[spec.shape]
261
+ return nil unless entry
262
+
263
+ entry[:render].call(self, spec.anchor, spec.base)
237
264
  end
238
265
 
239
266
  # Plus marker: two crossed line segments at the anchor.
@@ -384,6 +411,8 @@ module Ea
384
411
 
385
412
  [dx / len, dy / len]
386
413
  end
414
+ public :diamond_polygon, :triangle_polygon, :plus_path,
415
+ :arrow_path, :package_anchor_path
387
416
 
388
417
  def translate_point(p)
389
418
  return p unless @canvas
@@ -81,10 +81,7 @@ module Ea
81
81
  end
82
82
 
83
83
  def escaped_content
84
- content.gsub("&", "&amp;")
85
- .gsub("<", "&lt;")
86
- .gsub(">", "&gt;")
87
- .gsub("\"", "&quot;")
84
+ XmlEscape.call(content)
88
85
  end
89
86
  end
90
87
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Svg
5
+ module EaEmitter
6
+ # Maps UML visibility strings to EA's single-character symbols.
7
+ #
8
+ # Centralised so that adding a new visibility level (e.g.
9
+ # "implementation" in Java) changes one hash entry, not three
10
+ # case/when branches across separate renderer files.
11
+ #
12
+ # EA's convention:
13
+ # + public
14
+ # - private
15
+ # # protected
16
+ # ~ package
17
+ module VisibilitySymbol
18
+ SYMBOLS = {
19
+ "public" => "+",
20
+ "private" => "-",
21
+ "protected" => "#",
22
+ "package" => "~"
23
+ }.freeze
24
+
25
+ # @param visibility [String, nil] EA scope string ("Public",
26
+ # "Private", etc.). Case-insensitive; nil defaults to public.
27
+ # @param with_space [Boolean] append a trailing space (used
28
+ # by operation renderers: "+ method", not "+method").
29
+ # @return [String] the UML visibility symbol
30
+ def self.for(visibility, with_space: false)
31
+ symbol = SYMBOLS[(visibility || "public").downcase] || "+"
32
+ with_space ? "#{symbol} " : symbol
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -46,6 +46,7 @@ module Ea
46
46
  autoload :Label, "ea/svg/ea_emitter/label"
47
47
  autoload :Compartment, "ea/svg/ea_emitter/compartment"
48
48
  autoload :RenderContext, "ea/svg/ea_emitter/render_context"
49
+ autoload :VisibilitySymbol, "ea/svg/ea_emitter/visibility_symbol"
49
50
  end
50
51
  end
51
52
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ea"
4
3
  require "xmi"
5
- require "ea/sources/xmi/adapter"
6
4
 
7
5
  module Ea
8
6
  module Svg
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "nokogiri"
4
- require "ea/svg/parity/checker"
5
4
 
6
5
  module Ea
7
6
  module Svg
@@ -13,21 +13,42 @@ module Ea
13
13
  # `<upperValue value="M"/>`) — never a range string. This module
14
14
  # translates the EA form to a `{ lower:, upper: }` pair.
15
15
  #
16
- # Default-when-empty: EA's "no bound specified" maps to UML's
17
- # unspecified multiplicity, which Sparx renders as
18
- # `<lowerValue value="0"/>` and `<upperValue value="-1"/>`.
19
- # Always emitting both is required for round-trip parity with
20
- # real Sparx XMI (see TODO 26).
16
+ # Default-when-empty: blank input maps to UML's unspecified
17
+ # multiplicity, `{ lower: "0", upper: "*" }`. Whether those
18
+ # bounds reach the document is the caller's call — EA leaves an
19
+ # association end bare when its card field is blank.
21
20
  module Cardinality
22
21
  # Tokens EA uses for "unbounded". Matched case-insensitively.
23
- UNLIMITED_TOKENS = %w[* *-1 unbounded].freeze
22
+ UNLIMITED_TOKENS = %w[* *-1 -1 unbounded].freeze
24
23
 
25
- # UML defaults when EA carries no explicit bound.
24
+ # UML defaults when EA carries no explicit bound. EA's wire
25
+ # form for unlimited is "*" (never "-1" — the reference
26
+ # exports contain no value="-1" at all).
26
27
  DEFAULT_LOWER = "0"
27
- DEFAULT_UPPER = "-1"
28
+ DEFAULT_UPPER = "*"
29
+
30
+ # EA writes an explicit 1..1 for a Property whose t_attribute
31
+ # bound columns are BOTH blank, rather than falling back to the
32
+ # UML unspecified multiplicity.
33
+ DEFAULT_ATTRIBUTE_BOUNDS = { lower: "1", upper: "1" }.freeze
28
34
 
29
35
  module_function
30
36
 
37
+ # The bound pair for a t_attribute row. The 1..1 default is a
38
+ # property of the PAIR — with one column set, the missing side
39
+ # keeps the normal UML fallback, so `2` and a blank upper is
40
+ # 2..* rather than the invalid 2..1.
41
+ #
42
+ # @param lower [String, Integer, nil] t_attribute.lowerbound
43
+ # @param upper [String, Integer, nil] t_attribute.upperbound
44
+ # @return [Hash{Symbol=>String}] `{ lower:, upper: }`
45
+ def attribute_bounds(lower, upper)
46
+ both_blank = lower.to_s.strip.empty? && upper.to_s.strip.empty?
47
+ return DEFAULT_ATTRIBUTE_BOUNDS if both_blank
48
+
49
+ { lower: normalize_lower(lower), upper: normalize_upper(upper) }.freeze
50
+ end
51
+
31
52
  # @param raw [String, nil] e.g. "1..*", "0..1", "1", "*", nil
32
53
  # @return [Hash{Symbol=>String}] `{ lower:, upper: }` always
33
54
  # populated; never nil. Empty/nil input returns the UML default.
@@ -38,17 +59,14 @@ module Ea
38
59
  return parse_range(stripped) if stripped.include?("..")
39
60
 
40
61
  # Bare unlimited token (e.g. "*") means "many" — lower bound
41
- # is unspecified, which UML renders as 0..-1. Returning
42
- # `{ lower: "-1", upper: "-1" }` here would be invalid:
43
- # LiteralInteger cannot hold -1.
62
+ # is unspecified, which renders as 0..*.
44
63
  return defaults if UNLIMITED_TOKENS.include?(stripped.downcase)
45
64
 
46
- single = normalize_bound(stripped)
47
- { lower: single, upper: single }
65
+ { lower: stripped, upper: stripped }
48
66
  end
49
67
 
50
- # Normalise an upper-bound token: `*` / `unbounded` → `-1`
51
- # (UML LiteralUnlimitedNatural wire form).
68
+ # Normalise an upper-bound token: `*` / `unbounded` → `*`
69
+ # (the LiteralUnlimitedNatural wire form EA also uses).
52
70
  # @param raw [String, Integer, nil]
53
71
  # @return [String]
54
72
  def normalize_upper(raw)
@@ -57,17 +75,21 @@ module Ea
57
75
  stripped = raw.to_s.strip
58
76
  return DEFAULT_UPPER if stripped.empty?
59
77
 
60
- UNLIMITED_TOKENS.include?(stripped.downcase) ? "-1" : stripped
78
+ UNLIMITED_TOKENS.include?(stripped.downcase) ? "*" : stripped
61
79
  end
62
80
 
63
81
  # Normalise a lower-bound token: empty/nil → "0" (UML default).
82
+ # Unlimited tokens also → "0" — a lower bound serializes as
83
+ # uml:LiteralInteger, which cannot hold `*` or `-1`.
64
84
  # @param raw [String, Integer, nil]
65
85
  # @return [String]
66
86
  def normalize_lower(raw)
67
87
  return DEFAULT_LOWER if raw.nil?
68
88
 
69
89
  stripped = raw.to_s.strip
70
- stripped.empty? ? DEFAULT_LOWER : stripped
90
+ return DEFAULT_LOWER if stripped.empty?
91
+
92
+ UNLIMITED_TOKENS.include?(stripped.downcase) ? DEFAULT_LOWER : stripped
71
93
  end
72
94
 
73
95
  # ---- Internal helpers ----
@@ -78,17 +100,7 @@ module Ea
78
100
 
79
101
  def parse_range(stripped)
80
102
  lower, upper = stripped.split("..", 2)
81
- { lower: normalize_bound(lower), upper: normalize_bound(upper) }
82
- end
83
-
84
- # A single bound token (one side of `..` or a bare scalar).
85
- # Empty / `*` → UML unlimited (`-1`).
86
- def normalize_bound(token)
87
- return "-1" if token.nil?
88
- return "-1" if token.strip.empty?
89
-
90
- stripped = token.strip
91
- UNLIMITED_TOKENS.include?(stripped.downcase) ? "-1" : stripped
103
+ { lower: normalize_lower(lower), upper: normalize_upper(upper) }
92
104
  end
93
105
  end
94
106
  end
@@ -9,6 +9,11 @@ module Ea
9
9
  # - ID-derivation helpers (`xmi_id_for`, `end_xmi_id_for`) backed by
10
10
  # {GuidFormat}
11
11
  # - delegated database lookups (objects, packages, attributes, etc.)
12
+ # - the walk's position ordering (`sorted_by_position`), applied at
13
+ # the lookup for operation parameters so the UML tree and the
14
+ # extension block cannot disagree about their order. Attributes
15
+ # and operations are deliberately left for their callers to
16
+ # order — see `attributes_for`.
12
17
  #
13
18
  # The {Ea::Qea::Database} already maintains its own lookup indexes
14
19
  # (object-by-id, connectors-by-object, attributes-by-object, etc.).
@@ -33,6 +38,17 @@ module Ea
33
38
  GuidFormat.ea_guid_to_xmi_id(record.ea_guid, prefix: prefix)
34
39
  end
35
40
 
41
+ # Whether a record can anchor a synthesized id. EA derives a
42
+ # synthesized id's tail from its owner's GUID, so an owner
43
+ # without one yields a tailless id. Both the UML tree and the
44
+ # extension block ask this, so they cannot disagree about
45
+ # whether to emit the thing that id would have named.
46
+ # @param record [#ea_guid]
47
+ # @return [Boolean]
48
+ def identifiable?(record)
49
+ !record&.ea_guid.to_s.strip.empty?
50
+ end
51
+
36
52
  # @param connector_xmi_id [String]
37
53
  # @param side [Symbol] :source or :destination
38
54
  # @return [String]
@@ -66,6 +82,11 @@ module Ea
66
82
  database.objects_in_package(package_id)
67
83
  end
68
84
 
85
+ # Deliberately NOT ordered here, unlike params_for_operation:
86
+ # the extension block emits attributes in database order while
87
+ # the LI-bound preallocation sorts them by name, so each caller
88
+ # orders for itself. Sorting at this lookup would move export
89
+ # bytes.
69
90
  # @param object_id [Integer]
70
91
  # @return [Array<Ea::Qea::Models::EaAttribute>]
71
92
  def attributes_for(object_id)
@@ -78,10 +99,23 @@ module Ea
78
99
  database.operations_for_object(object_id)
79
100
  end
80
101
 
102
+ # t_operationparams is loaded with an unordered SELECT, and both
103
+ # the UML tree and the extension block emit parameters in Pos
104
+ # order. Ordering here means no caller can obtain them unordered.
81
105
  # @param operation_id [Integer]
82
106
  # @return [Array<Ea::Qea::Models::EaOperationParam>]
83
107
  def params_for_operation(operation_id)
84
- database.operation_params_for(operation_id)
108
+ sorted_by_position(database.operation_params_for(operation_id))
109
+ end
110
+
111
+ # ---- Ordering -----------------------------------------------------
112
+
113
+ # EA emits a record's children in tree-position order (TPos / Pos),
114
+ # ties broken by name.
115
+ # @param records [Array<Ea::Qea::Models::BaseModel>]
116
+ # @return [Array<Ea::Qea::Models::BaseModel>]
117
+ def sorted_by_position(records)
118
+ records.sort_by { |record| [record.sort_position, record.name.to_s] }
85
119
  end
86
120
 
87
121
  # Connectors where the object is on either side.