ea 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.complete/01-t-xref-parsing.md +1 -1
  3. data/TODO.complete/02-per-label-geometry.md +1 -1
  4. data/TODO.complete/03-object-properties-constraints.md +1 -1
  5. data/TODO.complete/04-operation-params.md +1 -1
  6. data/TODO.complete/05-pdata-flag-matrix.md +1 -1
  7. data/TODO.complete/06-emf-image-rendering.md +1 -1
  8. data/TODO.complete/07-auxiliary-tables.md +1 -1
  9. data/TODO.complete/10-mdg-stereotype-registry.md +1 -1
  10. data/TODO.complete/11-gml-xsd-generation.md +1 -1
  11. data/TODO.complete/13-shapescript-interpreter.md +12 -23
  12. data/TODO.complete/20-cli-diff.md +1 -1
  13. data/TODO.complete/21-cli-render.md +1 -1
  14. data/TODO.complete/22-cli-export.md +1 -1
  15. data/TODO.complete/23-cli-mdg.md +1 -1
  16. data/TODO.complete/30-headerlines-ocp-refactor.md +1 -1
  17. data/TODO.complete/32-spec-coverage.md +1 -1
  18. data/TODO.complete/33-blocked-parity-gaps.md +1 -1
  19. data/TODO.complete/36-stereotype-icon-wiring.md +1 -1
  20. data/TODO.complete/37-cli-lint.md +1 -1
  21. data/TODO.complete/38-cli-query.md +1 -1
  22. data/TODO.complete/39-cli-info.md +1 -1
  23. data/TODO.complete/40-ocl-evaluator.md +1 -1
  24. data/TODO.complete/41-round-trip-spec.md +1 -1
  25. data/TODO.complete/42-html-diff-report.md +1 -1
  26. data/TODO.complete/43-json-schema-export.md +1 -1
  27. data/TODO.complete/44-remaining-qea-tables.md +1 -1
  28. data/TODO.complete/45-shapescript-extensions.md +1 -1
  29. data/TODO.complete/46-xsd-consolidation.md +1 -1
  30. data/TODO.complete/47-headerlines-legacy-removal.md +1 -1
  31. data/TODO.complete/51-remove-respond-to.md +42 -15
  32. data/TODO.complete/52-xsd-decouple-fixtures.md +1 -1
  33. data/TODO.complete/54-wire-stereotype-icon-renderer.md +1 -1
  34. data/TODO.complete/55-wire-shapescript.md +1 -1
  35. data/TODO.complete/56-wire-emf-renderer.md +1 -1
  36. data/TODO.complete/57-wire-ocl-evaluator.md +1 -1
  37. data/TODO.complete/58-diff-modifications.md +1 -1
  38. data/TODO.complete/59-delete-toy-xmi-export.md +1 -1
  39. data/TODO.complete/60-curate-json-export.md +1 -1
  40. data/TODO.complete/61-plantuml-relationships.md +1 -1
  41. data/TODO.complete/62-changelog.md +1 -1
  42. data/TODO.complete/63-dependabot-fix.md +1 -1
  43. data/TODO.complete/64-qeatoxmi-connectors.md +9 -24
  44. data/TODO.complete/65-qeatoxmi-diagrams.md +9 -23
  45. data/TODO.complete/66-qeatoxmi-style-tags-docs.md +25 -18
  46. data/TODO.complete/67-ocl-collection-ops.md +12 -12
  47. data/TODO.complete/68-ocl-comparison-ops.md +16 -11
  48. data/TODO.complete/69-spec-new-models.md +13 -18
  49. data/TODO.complete/70-specs-for-all-models.md +21 -0
  50. data/TODO.complete/71-autoload-audit.md +26 -0
  51. data/TODO.complete/72-extension-serializer-ocp-naming.md +26 -0
  52. data/TODO.complete/73-ocl-validator-latent-bugs.md +50 -0
  53. data/TODO.complete/74-visibility-symbol-dry.md +39 -0
  54. data/TODO.complete/75-database-table-collection-registry.md +32 -0
  55. data/TODO.complete/76-stale-todo-cleanup.md +52 -0
  56. data/TODO.complete/77-eliminate-banned-patterns-from-specs.md +55 -0
  57. data/TODO.complete/78-mdg-stereotype-emission.md +66 -0
  58. data/TODO.complete/79-equivalence-fixes.md +57 -0
  59. data/TODO.complete/80-performance-extension-serializer-indexes.md +43 -0
  60. data/TODO.complete/81-dry-scope-guid-extraction.md +33 -0
  61. data/TODO.complete/82-public-send-audit-conclusion.md +25 -0
  62. data/TODO.complete/83-dry-shared-xml-escape-guid-format.md +52 -0
  63. data/TODO.complete/84-ocp-markers-shape-registry.md +54 -0
  64. data/TODO.complete/85-frozen-literal-rubocop-normalize-type-spec.md +38 -0
  65. data/TODO.complete/86-frozen-sweep-lint-query-specs.md +55 -0
  66. data/TODO.complete/STATUS.md +26 -5
  67. data/lib/ea/cli/app.rb +1 -0
  68. data/lib/ea/cli/command/export.rb +10 -1
  69. data/lib/ea/cli/command/mdg.rb +1 -10
  70. data/lib/ea/cli/command/render.rb +2 -1
  71. data/lib/ea/diff/html_reporter.rb +1 -5
  72. data/lib/ea/export/json/generator.rb +5 -6
  73. data/lib/ea/export/plantuml/generator.rb +5 -3
  74. data/lib/ea/guid_format.rb +91 -0
  75. data/lib/ea/mdg/document.rb +2 -1
  76. data/lib/ea/mdg/loader.rb +2 -1
  77. data/lib/ea/mdg/registry.rb +57 -2
  78. data/lib/ea/mdg/xml/applies_to.rb +1 -1
  79. data/lib/ea/mdg/xml/apply.rb +1 -1
  80. data/lib/ea/mdg/xml/content.rb +1 -1
  81. data/lib/ea/mdg/xml/documentation.rb +1 -1
  82. data/lib/ea/mdg/xml/stereotype.rb +1 -1
  83. data/lib/ea/mdg/xml/stereotypes.rb +1 -1
  84. data/lib/ea/mdg/xml/tag.rb +1 -1
  85. data/lib/ea/mdg/xml/tagged_values.rb +1 -1
  86. data/lib/ea/mdg/xml/technology.rb +1 -1
  87. data/lib/ea/mdg/xml/uml_profile.rb +1 -1
  88. data/lib/ea/mdg/xml/uml_profiles.rb +1 -1
  89. data/lib/ea/mdg/xml.rb +2 -1
  90. data/lib/ea/mdg.rb +2 -1
  91. data/lib/ea/model/ghost_label.rb +2 -1
  92. data/lib/ea/ocl/evaluator.rb +4 -2
  93. data/lib/ea/qea/database.rb +57 -2
  94. data/lib/ea/qea/factory/reference_resolver.rb +1 -1
  95. data/lib/ea/qea/models/ea_attribute.rb +1 -17
  96. data/lib/ea/qea/models/ea_connector.rb +3 -2
  97. data/lib/ea/qea/models/ea_object.rb +14 -9
  98. data/lib/ea/qea/models/ea_operation.rb +1 -17
  99. data/lib/ea/qea/models/ea_xref.rb +3 -0
  100. data/lib/ea/qea/models/scope_predicate.rb +29 -0
  101. data/lib/ea/qea/models.rb +1 -0
  102. data/lib/ea/qea/repositories/base_repository.rb +4 -1
  103. data/lib/ea/qea/validation/base_validator.rb +3 -48
  104. data/lib/ea/qea/validation/database/ocl_constraint_validator.rb +23 -13
  105. data/lib/ea/shapescript/renderer.rb +1 -3
  106. data/lib/ea/sources/qea/diagram_builder.rb +1 -0
  107. data/lib/ea/sources/qea/id_normalizer.rb +2 -2
  108. data/lib/ea/svg/ea_emitter/background.rb +2 -1
  109. data/lib/ea/svg/ea_emitter/element/attribute_line_builder.rb +1 -6
  110. data/lib/ea/svg/ea_emitter/element/operation_renderer.rb +2 -7
  111. data/lib/ea/svg/ea_emitter/elements.rb +1 -3
  112. data/lib/ea/svg/ea_emitter/ghost_labels.rb +2 -1
  113. data/lib/ea/svg/ea_emitter/label/end_label.rb +1 -6
  114. data/lib/ea/svg/ea_emitter/marker/package_import.rb +1 -1
  115. data/lib/ea/svg/ea_emitter/markers.rb +52 -23
  116. data/lib/ea/svg/ea_emitter/text_renderer.rb +1 -4
  117. data/lib/ea/svg/ea_emitter/visibility_symbol.rb +37 -0
  118. data/lib/ea/svg/ea_emitter.rb +1 -0
  119. data/lib/ea/svg/parity/source.rb +0 -2
  120. data/lib/ea/svg/parity/suite.rb +0 -1
  121. data/lib/ea/transformers/qea_to_xmi/cardinality.rb +40 -28
  122. data/lib/ea/transformers/qea_to_xmi/context.rb +35 -1
  123. data/lib/ea/transformers/qea_to_xmi/extension_serializer.rb +686 -0
  124. data/lib/ea/transformers/qea_to_xmi/guid_format.rb +5 -48
  125. data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +55 -54
  126. data/lib/ea/transformers/qea_to_xmi/primitive_types.rb +105 -0
  127. data/lib/ea/transformers/qea_to_xmi/profile_serializer.rb +163 -0
  128. data/lib/ea/transformers/qea_to_xmi/run_state.rb +7 -6
  129. data/lib/ea/transformers/qea_to_xmi/transformer.rb +396 -185
  130. data/lib/ea/transformers/qea_to_xmi/visibility.rb +30 -19
  131. data/lib/ea/transformers/qea_to_xmi.rb +5 -2
  132. data/lib/ea/transformers.rb +7 -2
  133. data/lib/ea/validation.rb +1 -1
  134. data/lib/ea/version.rb +1 -1
  135. data/lib/ea/xmi/liquid_drops/attribute_drop.rb +12 -4
  136. data/lib/ea/xmi/liquid_drops/klass_drop.rb +1 -2
  137. data/lib/ea/xmi/lookup_service.rb +4 -0
  138. data/lib/ea/xmi/parser.rb +30 -1
  139. data/lib/ea/xml_escape.rb +21 -0
  140. data/lib/ea.rb +2 -0
  141. metadata +28 -11
  142. data/lutaml-uml-0.5.1.gem +0 -0
@@ -0,0 +1,52 @@
1
+ # TODO.complete/83: DRY — shared XmlEscape + GuidFormat modules
2
+
3
+ ## Status: done
4
+
5
+ Two cross-subsystem DRY violations extracted into shared top-level
6
+ modules.
7
+
8
+ ## 1. Ea::XmlEscape
9
+
10
+ The XML entity escape (`& < > "` → entities) was duplicated in 4
11
+ files with subtle differences:
12
+
13
+ | File | Escapes | Nil-safe? |
14
+ |------|---------|-----------|
15
+ | `text_renderer.rb` | `& < > "` | no |
16
+ | `html_reporter.rb` | `& < > "` | yes |
17
+ | `shapescript/renderer.rb` | `& < >` (missing `"`!) | no |
18
+ | `extension_serializer.rb` | `& < > "` | no |
19
+
20
+ The ShapeScript version was **missing quote escaping** — a latent
21
+ bug that could produce broken SVG if a shape value contained `"`.
22
+
23
+ All four now call `Ea::XmlEscape.call(text)`:
24
+ - One canonical implementation
25
+ - Always escapes all 4 special chars
26
+ - Always nil-safe (via `to_s`)
27
+
28
+ Spec: `spec/ea/xml_escape_spec.rb` (8 examples).
29
+
30
+ ## 2. Ea::GuidFormat
31
+
32
+ GUID→XMI ID conversion was duplicated in 3 subsystems:
33
+
34
+ | File | Operation |
35
+ |------|-----------|
36
+ | `Transformers::QeaToXmi::GuidFormat` | full conversion (canonical) |
37
+ | `Sources::Qea::IdNormalizer` | inline `gsub(/[{}]/,"")` + `.tr("-","_")` |
38
+ | `Qea::Factory::ReferenceResolver` | inline `gsub(/[{}]/,"").upcase` |
39
+
40
+ Promoted `GuidFormat` to top-level `Ea::GuidFormat` (shared).
41
+ `Transformers::QeaToXmi::GuidFormat` is now an alias (backward
42
+ compatible — existing references don't break).
43
+
44
+ Added `strip_braces` method for callers that need just the brace
45
+ removal without prefix/dash conversion. `IdNormalizer` and
46
+ `ReferenceResolver` now use it.
47
+
48
+ ## Result
49
+
50
+ - Export output byte-identical (405898 bytes)
51
+ - 8 new specs (XmlEscape)
52
+ - 2211 examples, 0 failures, 55 pending
@@ -0,0 +1,54 @@
1
+ # TODO.complete/84: OCP — markers.rb shape dispatch → registry
2
+
3
+ ## Status: done
4
+
5
+ `Markers` had 3 case/when blocks all dispatching on the same
6
+ `spec.shape` enum. Adding a new marker shape required editing 3
7
+ methods — a classic OCP violation.
8
+
9
+ ## Before
10
+
11
+ ```ruby
12
+ def style_for(key)
13
+ case key
14
+ when :diamond_filled then "..."
15
+ when :triangle_open then "..."
16
+ when :connector_line then "..."
17
+ end
18
+ end
19
+
20
+ def style_key_for(spec)
21
+ case spec.shape
22
+ when :diamond then :diamond_filled
23
+ when :triangle then :triangle_open
24
+ when :plus then :connector_line
25
+ ...
26
+ end
27
+ end
28
+
29
+ def render_shape(spec)
30
+ case spec.shape
31
+ when :diamond then diamond_polygon(spec.anchor, spec.base)
32
+ when :triangle then triangle_polygon(spec.anchor, spec.base)
33
+ ...
34
+ end
35
+ end
36
+ ```
37
+
38
+ ## After
39
+
40
+ Two OCP registries: `SHAPE_REGISTRY` (shape → style_key + render
41
+ lambda) and `STYLE_MAP` (style_key → fill/opacity). Adding a new
42
+ marker shape = adding one entry to `SHAPE_REGISTRY`. Adding a new
43
+ style = adding one entry to `STYLE_MAP`. No existing code edited.
44
+
45
+ The render methods (`diamond_polygon`, `triangle_polygon`, etc.)
46
+ were promoted from private to public via `public :method` — they're
47
+ now called from the SHAPE_REGISTRY lambdas, so per the user's rule
48
+ ("promote tested methods to public") they must be accessible.
49
+
50
+ ## Acceptance
51
+
52
+ - All 293 SVG emitter specs pass (0 failures).
53
+ - Full suite: 2211 examples, 0 failures, 55 pending.
54
+ - Adding a new marker shape = 1 hash entry, not 3 case/when edits.
@@ -0,0 +1,38 @@
1
+ # TODO.complete/85: frozen_string_literal + rubocop + normalize_type spec
2
+
3
+ ## Status: done
4
+
5
+ Final code-quality pass.
6
+
7
+ ## 1. frozen_string_literal: true
8
+
9
+ 10 files were missing the `# frozen_string_literal: true` magic
10
+ comment (or had the wrong variant `# frozen_string: true` which
11
+ Ruby does not recognize):
12
+
13
+ - `lib/ea/validation.rb`
14
+ - `lib/ea/mdg.rb` (+ `document.rb`, `loader.rb`, `xml.rb`, `registry.rb`)
15
+ - `lib/ea/model/ghost_label.rb`
16
+ - `lib/ea/svg/ea_emitter/ghost_labels.rb`
17
+ - `lib/ea/svg/ea_emitter/background.rb`
18
+ - `lib/ea/cli/command/render.rb`
19
+
20
+ All now have the correct comment at line 1. The `registry.rb` had
21
+ a non-functional `# frozen_string: true` variant — replaced with
22
+ the correct one.
23
+
24
+ ## 2. Rubocop auto-correct
25
+
26
+ 30 auto-correctable offenses fixed across the new/modified files:
27
+ trailing commas, hash syntax normalization, etc.
28
+
29
+ ## 3. normalize_type spec
30
+
31
+ `ProfileSerializer#normalize_type` had no direct spec. Added 2
32
+ examples verifying Boolean → "Boolean" and unknown → "String".
33
+
34
+ ## Result
35
+
36
+ - 2213 examples, 0 failures, 55 pending.
37
+ - All lib files have `frozen_string_literal: true`.
38
+ - New code is rubocop-clean (style offenses auto-corrected).
@@ -0,0 +1,55 @@
1
+ # TODO.complete/86: frozen_string_literal sweep + Lint/Query specs
2
+
3
+ ## Status: done
4
+
5
+ ## 1. frozen_string_literal: wrong-variant sweep
6
+
7
+ Found 13 files with the **non-functional** `# frozen_string: true`
8
+ variant (Ruby only recognizes `# frozen_string_literal: true`).
9
+ These files' string literals were NOT frozen — silent correctness
10
+ gap.
11
+
12
+ Files fixed (wrong variant → correct):
13
+ - `lib/ea/svg/ea_emitter/marker/package_import.rb`
14
+ - `lib/ea/mdg/xml/*.rb` (11 files)
15
+ - `lib/ea/export/plantuml/generator.rb`
16
+
17
+ Also removed duplicate frozen comments from 8 files where my TODO
18
+ 85 fix prepended the correct variant without removing the old wrong
19
+ one:
20
+ - `lib/ea/mdg.rb`, `mdg/document.rb`, `mdg/loader.rb`, `mdg/xml.rb`
21
+ - `lib/ea/model/ghost_label.rb`
22
+ - `lib/ea/svg/ea_emitter/ghost_labels.rb`, `background.rb`
23
+ - `lib/ea/cli/command/render.rb`
24
+ - `lib/ea/validation.rb`
25
+
26
+ All 421 lib files now have exactly one `# frozen_string_literal: true`
27
+ at line 1.
28
+
29
+ ## 2. Lint subsystem specs (0 → 13 examples)
30
+
31
+ `spec/ea/lint/lint_spec.rb` — covers:
32
+ - `Offense#error?` / `#warning?` severity predicates
33
+ - `LintRule.name` CamelCase → snake_case conversion
34
+ - `LintRule.severity` default + override
35
+ - `LintRule#check` NotImplementedError
36
+ - `LintRule#offense` factory (tested through subclass, not `send`)
37
+ - `Engine#run` collects from all rules
38
+ - `Engine::DEFAULT_RULES` loads 5 built-in rules
39
+
40
+ ## 3. Query subsystem specs (0 → 11 examples)
41
+
42
+ `spec/ea/query/builder_spec.rb` — covers:
43
+ - `#classes`, `#interfaces`, `#packages` collection switching
44
+ - `#with_type` filtering
45
+ - `#in_package` by name
46
+ - `#named` exact match
47
+ - `#name_contains` case-insensitive substring
48
+ - Chaining + immutability (each method returns new Builder)
49
+ - Enumerable support via `#each`
50
+
51
+ ## Result
52
+
53
+ - 49 new examples across Lint + Query + Validation subsystems.
54
+ - All lib files have correct `frozen_string_literal: true`.
55
+ - No banned patterns in new specs (no `double`, no `send` to private).
@@ -11,20 +11,23 @@ gem runs anywhere Ruby runs.
11
11
  - **MDG technologies**: loader + registry + alias resolver functional.
12
12
  - **CLI commands**: 14 commands (parse, stats, list, diagrams, validate,
13
13
  convert, spa, svg, render, diff, export, mdg, lint, query, info).
14
- - **Export**: XMI (via full Transformers::QeaToXmi), JSON (curated),
15
- JSON Schema, PlantUML (with packages + relationships), XSD.
14
+ - **Export**: XMI (via full Transformers::QeaToXmi + ExtensionSerializer),
15
+ JSON (curated), JSON Schema, PlantUML (with packages + relationships),
16
+ XSD.
16
17
  - **ShapeScript**: parser + renderer (vars, arithmetic, conditionals,
17
18
  subshapes, labels). Wired into StereotypeIconRenderer.
18
- - **OCL evaluator**: invariant subset, wired into `ea validate`.
19
+ - **OCL evaluator**: invariant subset (exists, forAll, matches,
20
+ size, isEmpty, comparison ops), wired into `ea validate`.
19
21
  - **Lint**: 5 rules.
20
22
  - **Query DSL**: chainable Builder.
21
23
  - **Image**: t_image loading + EmfRenderer wired into Document emitter.
22
24
  - **Stereotype icons**: StereotypeIconRenderer wired as compartment.
23
25
  - **HeaderLines**: OCP provider chain.
24
26
  - **Diff**: text + HTML report; detects added/removed/renamed/modified.
25
- - **Specs**: 1986 examples, 0 failures, 55 pending.
27
+ - **Specs**: 2200+ examples, 0 failures, 55 pending.
26
28
  - **Code quality**: zero `respond_to?`, zero `instance_variable_get/set`,
27
- zero `require_relative` in lib code.
29
+ zero `require_relative` in lib code, zero internal `require` (all
30
+ autoload).
28
31
 
29
32
  ## Work streams — all completed (51–63: real completion work)
30
33
 
@@ -58,6 +61,24 @@ gem runs anywhere Ruby runs.
58
61
  | 62 | [62-changelog.md](62-changelog.md) | CHANGELOG.md | **completed** |
59
62
  | 63 | [63-dependabot-fix.md](63-dependabot-fix.md) | Dependabot vulnerability | **completed** (lychee-action v1 → v2) |
60
63
 
64
+ ### Stream G — Parity + Code Quality (this session)
65
+
66
+ | # | File | Title | Status |
67
+ |---|---|---|---|
68
+ | 64 | [64-qeatoxmi-connectors.md](64-qeatoxmi-connectors.md) | QeaToXmi emits connectors | **done** (via 66) |
69
+ | 65 | [65-qeatoxmi-diagrams.md](65-qeatoxmi-diagrams.md) | QeaToXmi emits diagrams | **done** (via 66) |
70
+ | 66 | [66-qeatoxmi-style-tags-docs.md](66-qeatoxmi-style-tags-docs.md) | QeaToXmi ExtensionSerializer | **done** |
71
+ | 67 | [67-ocl-collection-ops.md](67-ocl-collection-ops.md) | OCL size/isEmpty | **done** |
72
+ | 68 | [68-ocl-comparison-ops.md](68-ocl-comparison-ops.md) | OCL comparison operators | **done** |
73
+ | 69 | [69-spec-new-models.md](69-spec-new-models.md) | Specs for 13 new QEA models | **done** |
74
+ | 70 | [70-specs-for-all-models.md](70-specs-for-all-models.md) | Specs for 18 remaining models | **done** |
75
+ | 71 | [71-autoload-audit.md](71-autoload-audit.md) | Autoload audit | **done** |
76
+ | 72 | [72-extension-serializer-ocp-naming.md](72-extension-serializer-ocp-naming.md) | ExtensionSerializer OCP + naming | **done** |
77
+ | 73 | [73-ocl-validator-latent-bugs.md](73-ocl-validator-latent-bugs.md) | OCL validator latent bugs | **done** |
78
+ | 74 | [74-visibility-symbol-dry.md](74-visibility-symbol-dry.md) | Visibility symbol DRY extraction | **done** |
79
+ | 75 | [75-database-table-collection-registry.md](75-database-table-collection-registry.md) | Database table→collection registry (MECE) | **done** |
80
+ | 76 | [76-stale-todo-cleanup.md](76-stale-todo-cleanup.md) | Stale TODO status cleanup (40 items) | **done** |
81
+
61
82
  ### Bonus (no TODO file; found during 51)
62
83
 
63
84
  - Renamed `object_id` attribute to `ea_object_id` in 12 new QEA models
data/lib/ea/cli/app.rb CHANGED
@@ -120,6 +120,7 @@ module Ea
120
120
  desc "export SUB FILE", "Export model (SUB: xmi|json|plantuml|xsd)"
121
121
  option :output, **OUTPUT_OPTION, desc: "Output path"
122
122
  option :package, type: :string, desc: "Restrict to a package name"
123
+ option :mdg, type: :array, desc: "MDG technology XML files or directories (xmi only)"
123
124
  def export(sub, file)
124
125
  Command::Export.new(sub: sub, file: file, **symbolize(options)).call
125
126
  end
@@ -39,7 +39,7 @@ module Ea
39
39
  # a string. New formats add a new entry here without touching
40
40
  # this class body. Lambdas lazy-resolve autoloaded namespaces.
41
41
  EXPORTERS = {
42
- xmi: ->(model, **_o) { Ea::Transformers.qea_to_xmi(model) },
42
+ xmi: ->(model, **o) { Ea::Transformers.qea_to_xmi(model, mdg_registry: o[:mdg_registry]) },
43
43
  json: ->(model, **o) { Ea::Export::Json::Generator.call(model, **o) },
44
44
  "json-schema": ->(model, **o) { Ea::Export::JsonSchema::Generator.call(model, **o) },
45
45
  plantuml: ->(model, **o) { Ea::Export::PlantUml::Generator.call(model, **o) },
@@ -71,9 +71,18 @@ module Ea
71
71
  opts = {}
72
72
  opts[:target_namespace] = options[:target_namespace] if options[:target_namespace]
73
73
  opts[:prefix] = options[:prefix] if options[:prefix]
74
+ add_mdg_registry(opts)
74
75
  opts
75
76
  end
76
77
 
78
+ # Registry construction is xmi-only: the other exporters reject
79
+ # unknown keywords.
80
+ def add_mdg_registry(opts)
81
+ return unless options[:mdg] && sub == :xmi
82
+
83
+ opts[:mdg_registry] = Ea::Mdg::Registry.from_paths(options[:mdg])
84
+ end
85
+
77
86
  def model
78
87
  @model ||= load_database(file_path)
79
88
  end
@@ -39,16 +39,7 @@ module Ea
39
39
  end
40
40
 
41
41
  def build_registry
42
- registry = Ea::Mdg::Registry.new
43
- Dir.glob("spec/fixtures/mdg/**/*.xml").each do |path|
44
- begin
45
- doc = Ea::Mdg::Loader.from_path(path).document
46
- registry.register(doc)
47
- rescue StandardError => e
48
- warn "Skipped #{path}: #{e.message}" if ENV["EA_DEBUG"]
49
- end
50
- end
51
- registry
42
+ Ea::Mdg::Registry.from_paths(["spec/fixtures/mdg"])
52
43
  end
53
44
 
54
45
  def list_technologies
@@ -1,4 +1,5 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
+
2
3
 
3
4
  require "fileutils"
4
5
 
@@ -80,11 +80,7 @@ module Ea
80
80
  def escape(text)
81
81
  return "" if text.nil?
82
82
 
83
- text.to_s
84
- .gsub("&", "&amp;")
85
- .gsub("<", "&lt;")
86
- .gsub(">", "&gt;")
87
- .gsub('"', "&quot;")
83
+ XmlEscape.call(text)
88
84
  end
89
85
  end
90
86
  end
@@ -61,17 +61,16 @@ module Ea
61
61
  end
62
62
 
63
63
  def safe_id(record)
64
- pk = record.class.respond_to?(:primary_key_column) ?
65
- record.class.primary_key_column : nil
66
- return nil unless pk
64
+ return nil unless record.is_a?(Ea::Qea::Models::BaseModel)
65
+
66
+ pk = record.class.primary_key_column
67
+ return nil if pk.nil?
67
68
 
68
69
  record.to_hash[pk.to_s] || record.to_hash[pk.to_sym]
69
70
  end
70
71
 
71
72
  def record_name(record)
72
- return record.name if record.respond_to?(:name)
73
-
74
- record.to_hash["name"]
73
+ record.to_hash["name"] || record.to_hash[:name]
75
74
  end
76
75
 
77
76
  # Per-class projections: each entry is a proc returning a Hash.
@@ -1,4 +1,4 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Ea
4
4
  module Export
@@ -126,8 +126,10 @@ module Ea
126
126
  def enum_literals(enum)
127
127
  literals = model.collections[:enumeration_literals] ||
128
128
  model.collections[:enum_literals] || []
129
- # Enumeration literals are linked to the enum via object_id.
130
- literals.select { |l| l.respond_to?(:ea_object_id) && l.ea_object_id == enum.ea_object_id }
129
+ literals.select do |literal|
130
+ literal.is_a?(Ea::Qea::Models::EaAttribute) &&
131
+ literal.ea_object_id == enum.ea_object_id
132
+ end
131
133
  end
132
134
 
133
135
  def find_object(object_id)
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ # Shared conversions between Sparx EA GUID strings and XMI
5
+ # identifier strings. No state, no I/O — same input always
6
+ # yields same output.
7
+ #
8
+ # EA stores identifiers as `{ABCD-1234-...}` braced GUID strings.
9
+ # XMI serializations use unbraced, dash→underscore forms prefixed
10
+ # with `EAID_` (most elements) or `EAPK_` (packages at the model
11
+ # root).
12
+ #
13
+ # This module is shared across subsystems:
14
+ # - Transformers::QeaToXmi (XMI export)
15
+ # - Sources::Qea (model import / ID normalization)
16
+ # - Qea::Factory (UML bridge reference resolution)
17
+ #
18
+ # Previously each subsystem had its own inline gsub-based copy
19
+ # with subtle differences. Centralising here ensures one canonical
20
+ # conversion (DRY, MECE).
21
+ module GuidFormat
22
+ # Braces and dashes both become underscores; collapse runs of
23
+ # consecutive underscores so `{AB-CD}` → `AB_CD` (not `_AB_CD_`).
24
+ SEP = /[-{}]/
25
+
26
+ module_function
27
+
28
+ # @param ea_guid [String, nil] e.g. "{AB-CD-EF}"
29
+ # @param prefix [String] "EAID" (default) or "EAPK"
30
+ # @return [String, nil] e.g. "EAID_AB_CD_EF"
31
+ def ea_guid_to_xmi_id(ea_guid, prefix: "EAID")
32
+ return nil if ea_guid.nil? || ea_guid.empty?
33
+
34
+ clean = ea_guid.gsub(SEP, "_").gsub(/_+/, "_").gsub(/\A_/, "").gsub(/_\z/, "")
35
+ "#{prefix}_#{clean}"
36
+ end
37
+
38
+ # @param xmi_id [String, nil] e.g. "EAID_AB_CD_EF"
39
+ # @return [String, nil] e.g. "{AB-CD-EF}"
40
+ def xmi_id_to_ea_guid(xmi_id)
41
+ return nil if xmi_id.nil? || xmi_id.empty?
42
+
43
+ body = xmi_id.sub(/\A(?:EAID|EAPK)_/, "")
44
+ "{#{body.tr("_", "-")}}"
45
+ end
46
+
47
+ # Strip braces from an EA GUID without adding prefix or
48
+ # converting dashes. Used where the caller needs just the
49
+ # bare GUID body (e.g. reference_resolver).
50
+ # @param ea_guid [String, nil]
51
+ # @return [String] e.g. "AB-CD-EF" (dashes preserved)
52
+ def strip_braces(ea_guid)
53
+ return "" if ea_guid.nil?
54
+
55
+ ea_guid.to_s.gsub(/[{}]/, "")
56
+ end
57
+
58
+ # Build a member-end xmi:id for one side of a connector.
59
+ #
60
+ # Sparx EA's convention: take the connector's GUID, drop the first
61
+ # two hex characters of its first segment, and prepend `src` or
62
+ # `dst`.
63
+ #
64
+ # @param connector_xmi_id [String] e.g. "EAID_AB12CDEF_..."
65
+ # @param side [Symbol] :source or :destination
66
+ # @return [String] e.g. "EAID_src2CDEF_..."
67
+ def connector_end_xmi_id(connector_xmi_id, side:)
68
+ tag = side == :source ? "src" : "dst"
69
+ body = connector_xmi_id.sub(/\A(?:EAID|EAPK)_/, "")
70
+ first_segment, rest = body.split("_", 2)
71
+ trimmed = first_segment.length > 2 ? first_segment[2..] : first_segment
72
+ rest ? "EAID_#{tag}#{trimmed}_#{rest}" : "EAID_#{tag}#{trimmed}"
73
+ end
74
+
75
+ # Build the xmi:id for an operation's synthesized return parameter.
76
+ #
77
+ # Sparx EA's convention: take the operation's own id and replace its
78
+ # first GUID segment with `RETURNID`. An id carrying no further
79
+ # segments has nothing to drop, so its only segment is kept. The
80
+ # parameter's ea_guid is the braced form of this id, so callers
81
+ # derive it with {xmi_id_to_ea_guid} rather than spelling the shape
82
+ # a second time.
83
+ #
84
+ # @param operation_xmi_id [String] e.g. "EAID_AB12CDEF_3456_..."
85
+ # @return [String] e.g. "EAID_RETURNID_3456_..."
86
+ def return_parameter_xmi_id(operation_xmi_id)
87
+ body = operation_xmi_id.sub(/\A(?:EAID|EAPK)_/, "")
88
+ "EAID_RETURNID_#{body.split("_", 2).last}"
89
+ end
90
+ end
91
+ end
@@ -1,4 +1,5 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
+
2
3
 
3
4
  module Ea
4
5
  module Mdg
data/lib/ea/mdg/loader.rb CHANGED
@@ -1,4 +1,5 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
+
2
3
 
3
4
  module Ea
4
5
  module Mdg
@@ -1,4 +1,4 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Ea
4
4
  module Mdg
@@ -26,13 +26,51 @@ module Ea
26
26
  @documents = []
27
27
  end
28
28
 
29
+ # Build a registry from MDG XML files and/or directories
30
+ # (directories are globbed recursively). Unloadable files are
31
+ # skipped — MDG discovery is a boundary where partial input is
32
+ # normal (set EA_DEBUG=1 to see what was skipped).
33
+ #
34
+ # @param paths [Array<String>]
35
+ # @return [Ea::Mdg::Registry]
36
+ def self.from_paths(paths)
37
+ new.tap do |registry|
38
+ paths.flat_map { |path| expand_mdg_path(path) }.each do |file|
39
+ registry.register(Loader.from_path(file).document)
40
+ rescue StandardError => e
41
+ warn "Skipped MDG #{file}: #{e.message}" if ENV["EA_DEBUG"]
42
+ end
43
+ end
44
+ end
45
+
46
+ def self.expand_mdg_path(path)
47
+ File.directory?(path) ? Dir.glob(File.join(path, "**/*.xml")).sort : [path]
48
+ end
49
+
29
50
  # Register an MDG Document. Subsequent registrations of the
30
51
  # same technology_name replace the prior one (last-wins).
52
+ #
53
+ # Also registers the technology with
54
+ # `Lutaml::Model::GlobalRegister` so it can be looked up via
55
+ # the framework-level register API. This keeps MDG swapping
56
+ # OCP-compliant: callers can query either Ea::Mdg::Registry
57
+ # or Lutaml::Model::GlobalRegister to find loaded MDGs.
31
58
  def register(document)
32
- @documents.reject! { |d| d.technology_name == document.technology_name }
59
+ technology = document.technology_name
60
+ @documents.reject! { |d| d.technology_name == technology }
33
61
  @documents << document
62
+ register_with_lutaml(technology, document)
34
63
  end
35
64
 
65
+ # Remove an MDG Document by technology name. Also unregisters
66
+ # from Lutaml::Model::GlobalRegister.
67
+ def unregister(technology_name)
68
+ @documents.reject! { |d| d.technology_name == technology_name }
69
+ lutaml_register_id = lutaml_id_for(technology_name)
70
+ Lutaml::Model::GlobalRegister.remove(lutaml_register_id)
71
+ end
72
+ alias remove unregister
73
+
36
74
  def each(&block)
37
75
  @documents.each(&block)
38
76
  end
@@ -93,6 +131,23 @@ module Ea
93
131
 
94
132
  result
95
133
  end
134
+
135
+ private
136
+
137
+ # Registers the MDG with Lutaml::Model::GlobalRegister under a
138
+ # stable, namespaced id. The register holds no model classes
139
+ # directly (MDG stereotypes are domain concepts, not lutaml-model
140
+ # types), but the registration makes the MDG discoverable via
141
+ # the framework's standard register API.
142
+ def register_with_lutaml(technology_name, _document)
143
+ lutaml_register_id = lutaml_id_for(technology_name)
144
+ register = Lutaml::Model::Register.new(lutaml_register_id)
145
+ Lutaml::Model::GlobalRegister.register(register)
146
+ end
147
+
148
+ def lutaml_id_for(technology_name)
149
+ :"ea_mdg_#{technology_name.to_s.downcase.gsub(/[^a-z0-9]/, "_")}"
150
+ end
96
151
  end
97
152
  end
98
153
  end
@@ -1,4 +1,4 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
4
 
@@ -1,4 +1,4 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
4
 
@@ -1,4 +1,4 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
4
 
@@ -1,4 +1,4 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
4
 
@@ -1,4 +1,4 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
4
 
@@ -1,4 +1,4 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
4
 
@@ -1,4 +1,4 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
4
 
@@ -1,4 +1,4 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
4
 
@@ -1,4 +1,4 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
4
 
@@ -1,4 +1,4 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
4
 
@@ -1,4 +1,4 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
4
 
data/lib/ea/mdg/xml.rb CHANGED
@@ -1,4 +1,5 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
+
2
3
 
3
4
  require "lutaml/model"
4
5
 
data/lib/ea/mdg.rb CHANGED
@@ -1,4 +1,5 @@
1
- # frozen_string: true
1
+ # frozen_string_literal: true
2
+
2
3
 
3
4
  require "lutaml/model"
4
5