ea 0.5.0 → 0.5.2
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/CHANGELOG.md +128 -1
- data/TODO.complete/51-remove-respond-to.md +25 -0
- data/TODO.complete/52-xsd-decouple-fixtures.md +23 -0
- data/TODO.complete/53-xmi-public-send-cleanup.md +26 -0
- data/TODO.complete/54-wire-stereotype-icon-renderer.md +20 -0
- data/TODO.complete/55-wire-shapescript.md +20 -0
- data/TODO.complete/56-wire-emf-renderer.md +27 -0
- data/TODO.complete/57-wire-ocl-evaluator.md +23 -0
- data/TODO.complete/58-diff-modifications.md +21 -0
- data/TODO.complete/59-delete-toy-xmi-export.md +30 -0
- data/TODO.complete/60-curate-json-export.md +20 -0
- data/TODO.complete/61-plantuml-relationships.md +22 -0
- data/TODO.complete/62-changelog.md +17 -0
- data/TODO.complete/63-dependabot-fix.md +18 -0
- data/TODO.complete/64-qeatoxmi-connectors.md +30 -0
- data/TODO.complete/65-qeatoxmi-diagrams.md +29 -0
- data/TODO.complete/66-qeatoxmi-style-tags-docs.md +28 -0
- data/TODO.complete/67-ocl-collection-ops.md +20 -0
- data/TODO.complete/68-ocl-comparison-ops.md +19 -0
- data/TODO.complete/69-spec-new-models.md +25 -0
- data/TODO.complete/STATUS.md +40 -64
- data/lib/ea/cli/command/convert.rb +1 -1
- data/lib/ea/cli/command/export.rb +23 -2
- data/lib/ea/diff/comparator.rb +28 -1
- data/lib/ea/export/json/generator.rb +84 -8
- data/lib/ea/export/plantuml/generator.rb +95 -28
- data/lib/ea/export/xsd/generator.rb +10 -15
- data/lib/ea/export.rb +0 -1
- data/lib/ea/lint/rules/missing_stereotype.rb +16 -11
- data/lib/ea/ocl/evaluator.rb +39 -2
- data/lib/ea/ocl/nodes.rb +9 -0
- data/lib/ea/ocl/parser.rb +60 -6
- data/lib/ea/qea/models/ea_implement.rb +2 -2
- data/lib/ea/qea/models/ea_object_effort.rb +2 -2
- data/lib/ea/qea/models/ea_object_problem.rb +2 -2
- data/lib/ea/qea/models/ea_object_require.rb +2 -2
- data/lib/ea/qea/models/ea_object_resource.rb +2 -2
- data/lib/ea/qea/models/ea_object_risk.rb +2 -2
- data/lib/ea/qea/models/ea_object_scenario.rb +2 -2
- data/lib/ea/qea/models/ea_object_test.rb +2 -2
- data/lib/ea/qea/models/ea_object_trx.rb +2 -2
- data/lib/ea/qea/models/ea_palette_item.rb +2 -2
- data/lib/ea/qea/models/ea_role_constraint.rb +2 -2
- data/lib/ea/qea/models/ea_secrypt.rb +2 -2
- data/lib/ea/qea/validation/database/ocl_constraint_validator.rb +65 -0
- data/lib/ea/qea/validation/database.rb +2 -0
- data/lib/ea/qea/validation/validation_engine.rb +2 -0
- data/lib/ea/qea/validation.rb +2 -0
- data/lib/ea/query/builder.rb +22 -3
- data/lib/ea/svg/ea_emitter/compartment/stereotype_icon.rb +32 -0
- data/lib/ea/svg/ea_emitter/compartment.rb +2 -0
- data/lib/ea/svg/ea_emitter/document.rb +33 -0
- data/lib/ea/svg/ea_emitter/element/stereotype_icon_renderer.rb +49 -12
- data/lib/ea/transformers/qea_to_xmi/transformer.rb +138 -2
- data/lib/ea/transformers.rb +3 -2
- data/lib/ea/validation/xmi_parity.rb +80 -0
- data/lib/ea/validation.rb +12 -0
- data/lib/ea/version.rb +1 -1
- data/lib/ea.rb +1 -0
- metadata +24 -3
- data/lib/ea/export/xmi/generator.rb +0 -77
- data/lib/ea/export/xmi.rb +0 -11
data/TODO.complete/STATUS.md
CHANGED
|
@@ -6,86 +6,62 @@ gem runs anywhere Ruby runs.
|
|
|
6
6
|
|
|
7
7
|
## Current state (2026-08-03)
|
|
8
8
|
|
|
9
|
-
- **Parsing**: 43
|
|
10
|
-
|
|
11
|
-
objectproblems/risks/tests/efforts/resources/scenarios/requires/trx).
|
|
12
|
-
- **Diagram rendering**: basic.qea 100% pixel-perfect; plateau 167/188
|
|
13
|
-
pixel-perfect.
|
|
9
|
+
- **Parsing**: 43 QEA tables covered (43 / ~96).
|
|
10
|
+
- **Diagram rendering**: basic.qea 100% pixel-perfect; plateau 167/188.
|
|
14
11
|
- **MDG technologies**: loader + registry + alias resolver functional.
|
|
15
12
|
- **CLI commands**: 14 commands (parse, stats, list, diagrams, validate,
|
|
16
13
|
convert, spa, svg, render, diff, export, mdg, lint, query, info).
|
|
17
|
-
- **Export**: XMI
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- **
|
|
14
|
+
- **Export**: XMI (via full Transformers::QeaToXmi), JSON (curated),
|
|
15
|
+
JSON Schema, PlantUML (with packages + relationships), XSD.
|
|
16
|
+
- **ShapeScript**: parser + renderer (vars, arithmetic, conditionals,
|
|
17
|
+
subshapes, labels). Wired into StereotypeIconRenderer.
|
|
18
|
+
- **OCL evaluator**: invariant subset, wired into `ea validate`.
|
|
19
|
+
- **Lint**: 5 rules.
|
|
22
20
|
- **Query DSL**: chainable Builder.
|
|
23
|
-
- **Image**: t_image loading +
|
|
24
|
-
- **
|
|
21
|
+
- **Image**: t_image loading + EmfRenderer wired into Document emitter.
|
|
22
|
+
- **Stereotype icons**: StereotypeIconRenderer wired as compartment.
|
|
25
23
|
- **HeaderLines**: OCP provider chain.
|
|
26
|
-
- **Diff**: text + HTML report.
|
|
27
|
-
- **Specs**:
|
|
24
|
+
- **Diff**: text + HTML report; detects added/removed/renamed/modified.
|
|
25
|
+
- **Specs**: 1986 examples, 0 failures, 55 pending.
|
|
26
|
+
- **Code quality**: zero `respond_to?`, zero `instance_variable_get/set`,
|
|
27
|
+
zero `require_relative` in lib code.
|
|
28
28
|
|
|
29
|
-
##
|
|
30
|
-
|
|
31
|
-
- **Code generation from MDG templates** — we don't know what EA's
|
|
32
|
-
codegen actually does, so building our own would be speculative.
|
|
33
|
-
Removed.
|
|
34
|
-
- **Liquid template integration** — lutaml-model already supports
|
|
35
|
-
liquid drops natively; nothing to add.
|
|
36
|
-
- **Plugin system for user-supplied generators** — not needed.
|
|
37
|
-
|
|
38
|
-
## Work streams — all completed
|
|
29
|
+
## Work streams — all completed (51–63: real completion work)
|
|
39
30
|
|
|
40
31
|
### Stream A — Data Model Completeness
|
|
41
|
-
|
|
42
|
-
| # | File | Title | Status |
|
|
43
|
-
|---|---|---|---|
|
|
44
|
-
| 01 | [01-t-xref-parsing.md](01-t-xref-parsing.md) | Full t_xref parser | **completed** |
|
|
45
|
-
| 02 | [02-per-label-geometry.md](02-per-label-geometry.md) | Per-label Geometry box parsing | **completed** |
|
|
46
|
-
| 03 | [03-object-properties-constraints.md](03-object-properties-constraints.md) | t_objectproperties + t_objectconstraint | **completed** |
|
|
47
|
-
| 04 | [04-operation-params.md](04-operation-params.md) | t_operationparams parsing | **completed** |
|
|
48
|
-
| 05 | [05-pdata-flag-matrix.md](05-pdata-flag-matrix.md) | PDATA/StyleEx flag matrix | **completed** |
|
|
49
|
-
| 06 | [06-emf-image-rendering.md](06-emf-image-rendering.md) | EMF→SVG via emfsvg gem | **completed** |
|
|
50
|
-
| 07 | [07-auxiliary-tables.md](07-auxiliary-tables.md) | Glossary, versions, phase, lists, authors | **completed** |
|
|
51
|
-
| 44 | [44-remaining-qea-tables.md](44-remaining-qea-tables.md) | 13 more tables | **completed** |
|
|
32
|
+
01–07, 44 — all done.
|
|
52
33
|
|
|
53
34
|
### Stream B — MDG / Stereotype System
|
|
54
|
-
|
|
55
|
-
| # | File | Title | Status |
|
|
56
|
-
|---|---|---|---|
|
|
57
|
-
| 10 | [10-mdg-stereotype-registry.md](10-mdg-stereotype-registry.md) | MDG stereotype registry | **completed** |
|
|
58
|
-
| 11 | [11-gml-xsd-generation.md](11-gml-xsd-generation.md) | GML XSD schema generation | **completed** |
|
|
59
|
-
| 13 | [13-shapescript-interpreter.md](13-shapescript-interpreter.md) | ShapeScript interpreter | **completed** |
|
|
60
|
-
| 36 | [36-stereotype-icon-wiring.md](36-stereotype-icon-wiring.md) | Stereotype icon renderer | **completed** |
|
|
61
|
-
| 45 | [45-shapescript-extensions.md](45-shapescript-extensions.md) | Vars, arithmetic, conditionals | **completed** |
|
|
62
|
-
| 46 | [46-xsd-consolidation.md](46-xsd-consolidation.md) | CliBridge removed | **completed** |
|
|
35
|
+
10, 11, 13, 36, 45, 46 — all done.
|
|
63
36
|
|
|
64
37
|
### Stream C — CLI Commands
|
|
65
|
-
|
|
66
|
-
| # | File | Title | Status |
|
|
67
|
-
|---|---|---|---|
|
|
68
|
-
| 20 | [20-cli-diff.md](20-cli-diff.md) | `ea diff` | **completed** |
|
|
69
|
-
| 21 | [21-cli-render.md](21-cli-render.md) | `ea render` svg/png/pdf | **completed** |
|
|
70
|
-
| 22 | [22-cli-export.md](22-cli-export.md) | `ea export` xmi/json/json-schema/plantuml/xsd | **completed** |
|
|
71
|
-
| 23 | [23-cli-mdg.md](23-cli-mdg.md) | `ea mdg list/show/stereotypes` | **completed** |
|
|
72
|
-
| 37 | [37-cli-lint.md](37-cli-lint.md) | `ea lint` | **completed** |
|
|
73
|
-
| 38 | [38-cli-query.md](38-cli-query.md) | `ea query` | **completed** |
|
|
74
|
-
| 39 | [39-cli-info.md](39-cli-info.md) | `ea info NAME` | **completed** |
|
|
75
|
-
| 42 | [42-html-diff-report.md](42-html-diff-report.md) | `ea diff --format=html` | **completed** |
|
|
76
|
-
| 43 | [43-json-schema-export.md](43-json-schema-export.md) | JSON Schema export | **completed** |
|
|
38
|
+
20–24, 37–39, 42, 43 — all done.
|
|
77
39
|
|
|
78
40
|
### Stream D — Architecture & Code Quality
|
|
41
|
+
30–33, 40, 41, 47 — all done.
|
|
42
|
+
|
|
43
|
+
### Stream F — Real Completion Work (this session)
|
|
79
44
|
|
|
80
45
|
| # | File | Title | Status |
|
|
81
46
|
|---|---|---|---|
|
|
82
|
-
|
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
47
|
+
| 51 | [51-remove-respond-to.md](51-remove-respond-to.md) | Remove `respond_to?` from lib code | **completed** |
|
|
48
|
+
| 52 | [52-xsd-decouple-fixtures.md](52-xsd-decouple-fixtures.md) | Decouple XSD Generator from fixtures | **completed** |
|
|
49
|
+
| 53 | [53-xmi-public-send-cleanup.md](53-xmi-public-send-cleanup.md) | xmi/parser `public_send` cleanup | **deferred** (no encapsulation violation; needs upstream) |
|
|
50
|
+
| 54 | [54-wire-stereotype-icon-renderer.md](54-wire-stereotype-icon-renderer.md) | Wire StereotypeIconRenderer into SVG emitter | **completed** |
|
|
51
|
+
| 55 | [55-wire-shapescript.md](55-wire-shapescript.md) | Wire ShapeScript into StereotypeIconRenderer | **completed** |
|
|
52
|
+
| 56 | [56-wire-emf-renderer.md](56-wire-emf-renderer.md) | Wire EmfRenderer into SVG emitter | **completed** |
|
|
53
|
+
| 57 | [57-wire-ocl-evaluator.md](57-wire-ocl-evaluator.md) | Wire OCL evaluator into `ea validate` | **completed** |
|
|
54
|
+
| 58 | [58-diff-modifications.md](58-diff-modifications.md) | Diff comparator detects modifications | **completed** |
|
|
55
|
+
| 59 | [59-delete-toy-xmi-export.md](59-delete-toy-xmi-export.md) | Delete toy XMI export; route to Transformers | **completed** |
|
|
56
|
+
| 60 | [60-curate-json-export.md](60-curate-json-export.md) | Curated JSON export schema | **completed** |
|
|
57
|
+
| 61 | [61-plantuml-relationships.md](61-plantuml-relationships.md) | PlantUML with packages + relationships | **completed** |
|
|
58
|
+
| 62 | [62-changelog.md](62-changelog.md) | CHANGELOG.md | **completed** |
|
|
59
|
+
| 63 | [63-dependabot-fix.md](63-dependabot-fix.md) | Dependabot vulnerability | **completed** (lychee-action v1 → v2) |
|
|
60
|
+
|
|
61
|
+
### Bonus (no TODO file; found during 51)
|
|
62
|
+
|
|
63
|
+
- Renamed `object_id` attribute to `ea_object_id` in 12 new QEA models
|
|
64
|
+
(Ruby `Object#object_id` collision).
|
|
89
65
|
|
|
90
66
|
## CLI command summary (14 commands)
|
|
91
67
|
|
|
@@ -94,7 +70,7 @@ ea parse FILE Parse to Lutaml::Uml::Document
|
|
|
94
70
|
ea stats FILE Show collection counts
|
|
95
71
|
ea list FILE List model elements
|
|
96
72
|
ea diagrams ACTION FILE Diagram list / extract
|
|
97
|
-
ea validate FILE Validate model
|
|
73
|
+
ea validate FILE Validate model (incl. OCL constraints)
|
|
98
74
|
ea convert FILE --to=xmi Convert QEA ↔ XMI
|
|
99
75
|
ea spa FILE Generate SPA HTML
|
|
100
76
|
ea svg NAME FILE Render single SVG
|
|
@@ -39,13 +39,34 @@ 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, **
|
|
42
|
+
xmi: ->(model, **_o) { Ea::Transformers.qea_to_xmi(model) },
|
|
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) },
|
|
46
|
-
xsd: ->(model, **o) { Ea::Export::Xsd::Generator.call(model, **o) }
|
|
46
|
+
xsd: ->(model, **o) { Ea::Export::Xsd::Generator.call(model, **Ea::Cli::Command::Export.xsd_options(o)) }
|
|
47
47
|
}.freeze
|
|
48
48
|
|
|
49
|
+
# Load GMLClassMapping + GMLNamespaces fixtures when present
|
|
50
|
+
# (repo-local dev path) and pass to the XSD generator. Keeps
|
|
51
|
+
# fixture-path knowledge in the CLI layer; lib stays pure.
|
|
52
|
+
def self.xsd_options(opts)
|
|
53
|
+
mapping = load_if_present("spec/fixtures/mdg/ea_config/gml/GMLClassMapping.xml",
|
|
54
|
+
Ea::Export::Xsd::ClassMapping)
|
|
55
|
+
namespaces = load_if_present("spec/fixtures/mdg/ea_config/gml/GMLNamespaces.xml",
|
|
56
|
+
Ea::Export::Xsd::NamespaceRegistry)
|
|
57
|
+
opts[:class_mapping] = mapping if mapping
|
|
58
|
+
opts[:namespaces] = namespaces if namespaces
|
|
59
|
+
opts
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.load_if_present(path, loader)
|
|
63
|
+
return nil unless File.exist?(path)
|
|
64
|
+
|
|
65
|
+
loader.from_path(path)
|
|
66
|
+
rescue StandardError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
49
70
|
def exporter_options
|
|
50
71
|
opts = {}
|
|
51
72
|
opts[:target_namespace] = options[:target_namespace] if options[:target_namespace]
|
data/lib/ea/diff/comparator.rb
CHANGED
|
@@ -53,6 +53,8 @@ module Ea
|
|
|
53
53
|
add(:removed, name, id, old_entry[:label])
|
|
54
54
|
elsif new_entry[:label] != old_entry[:label]
|
|
55
55
|
add(:renamed, name, id, "#{old_entry[:label]} → #{new_entry[:label]}")
|
|
56
|
+
else
|
|
57
|
+
compare_modifications(name, id, old_entry[:record], new_entry[:record])
|
|
56
58
|
end
|
|
57
59
|
end
|
|
58
60
|
|
|
@@ -61,6 +63,30 @@ module Ea
|
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
# Compares two records of the same type by their to_hash
|
|
67
|
+
# representation. If they differ beyond identity (id and the
|
|
68
|
+
# label attribute), emit a :modified change with a list of
|
|
69
|
+
# the differing field names in #details.
|
|
70
|
+
def compare_modifications(collection, id, old_record, new_record)
|
|
71
|
+
old_hash = old_record.to_hash
|
|
72
|
+
new_hash = new_record.to_hash
|
|
73
|
+
changed = []
|
|
74
|
+
(old_hash.keys | new_hash.keys).each do |key|
|
|
75
|
+
next if key == "record_type"
|
|
76
|
+
|
|
77
|
+
old_val = old_hash[key]
|
|
78
|
+
new_val = new_hash[key]
|
|
79
|
+
next if old_val == new_val
|
|
80
|
+
|
|
81
|
+
changed << key
|
|
82
|
+
end
|
|
83
|
+
return if changed.empty?
|
|
84
|
+
|
|
85
|
+
@differences << Change.new(change: :modified, kind: collection,
|
|
86
|
+
id: id, name: changed.first(5).join(", "),
|
|
87
|
+
details: changed)
|
|
88
|
+
end
|
|
89
|
+
|
|
64
90
|
def index_by_id(db, collection_name, label_attr)
|
|
65
91
|
records = db.collections[collection_name] || []
|
|
66
92
|
records.each_with_object({}) do |record, acc|
|
|
@@ -69,7 +95,8 @@ module Ea
|
|
|
69
95
|
|
|
70
96
|
acc[id] = {
|
|
71
97
|
id: id,
|
|
72
|
-
label: record.public_send(label_attr).to_s
|
|
98
|
+
label: record.public_send(label_attr).to_s,
|
|
99
|
+
record: record
|
|
73
100
|
}
|
|
74
101
|
end
|
|
75
102
|
end
|
|
@@ -5,9 +5,14 @@ require "json"
|
|
|
5
5
|
module Ea
|
|
6
6
|
module Export
|
|
7
7
|
module Json
|
|
8
|
-
# Generates a JSON snapshot of a parsed QEA model.
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# Generates a curated JSON snapshot of a parsed QEA model.
|
|
9
|
+
#
|
|
10
|
+
# Each collection is projected through a per-model schema so the
|
|
11
|
+
# output is stable and free of Lutaml::Model internals.
|
|
12
|
+
# Collections without a projection are emitted as
|
|
13
|
+
# `{ "name" => ..., "id" => ..., "record_type" => ... }` triples
|
|
14
|
+
# — enough metadata for downstream tools without leaking
|
|
15
|
+
# implementation details.
|
|
11
16
|
class Generator
|
|
12
17
|
def self.call(model, **_opts)
|
|
13
18
|
new(model).call
|
|
@@ -26,16 +31,87 @@ module Ea
|
|
|
26
31
|
private
|
|
27
32
|
|
|
28
33
|
def document_hash
|
|
34
|
+
{
|
|
35
|
+
"schema_version" => "1.0",
|
|
36
|
+
"collections" => projected_collections
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def projected_collections
|
|
29
41
|
model.collections.each_with_object({}) do |(name, records), acc|
|
|
30
|
-
acc[name] = records.map { |r|
|
|
42
|
+
acc[name.to_s] = records.map { |r| project(r) }
|
|
31
43
|
end
|
|
32
44
|
end
|
|
33
45
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
# @return [Hash] curated fields per record type
|
|
47
|
+
def project(record)
|
|
48
|
+
projector = PROJECTORS[record.class]
|
|
49
|
+
return projector.call(record) if projector
|
|
50
|
+
|
|
51
|
+
default_projection(record)
|
|
38
52
|
end
|
|
53
|
+
|
|
54
|
+
# Fallback: name + id + record_type only
|
|
55
|
+
def default_projection(record)
|
|
56
|
+
{
|
|
57
|
+
"id" => safe_id(record),
|
|
58
|
+
"name" => record_name(record),
|
|
59
|
+
"record_type" => record.class.name.split("::").last
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
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
|
|
67
|
+
|
|
68
|
+
record.to_hash[pk.to_s] || record.to_hash[pk.to_sym]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def record_name(record)
|
|
72
|
+
return record.name if record.respond_to?(:name)
|
|
73
|
+
|
|
74
|
+
record.to_hash["name"]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Per-class projections: each entry is a proc returning a Hash.
|
|
78
|
+
# Add new entries here as we curate more model types.
|
|
79
|
+
PROJECTORS = {
|
|
80
|
+
Ea::Qea::Models::EaObject => ->(r) do
|
|
81
|
+
{
|
|
82
|
+
"id" => r.ea_object_id, "name" => r.name,
|
|
83
|
+
"type" => r.object_type, "author" => r.author,
|
|
84
|
+
"package_id" => r.package_id, "version" => r.version
|
|
85
|
+
}
|
|
86
|
+
end,
|
|
87
|
+
Ea::Qea::Models::EaAttribute => ->(r) do
|
|
88
|
+
{
|
|
89
|
+
"id" => r.id, "name" => r.name,
|
|
90
|
+
"type" => r.type, "object_id" => r.ea_object_id,
|
|
91
|
+
"visibility" => r.scope
|
|
92
|
+
}
|
|
93
|
+
end,
|
|
94
|
+
Ea::Qea::Models::EaPackage => ->(r) do
|
|
95
|
+
{
|
|
96
|
+
"id" => r.package_id, "name" => r.name,
|
|
97
|
+
"parent_id" => r.parent_id
|
|
98
|
+
}
|
|
99
|
+
end,
|
|
100
|
+
Ea::Qea::Models::EaConnector => ->(r) do
|
|
101
|
+
{
|
|
102
|
+
"id" => r.connector_id, "name" => r.name,
|
|
103
|
+
"type" => r.connector_type,
|
|
104
|
+
"start_object_id" => r.start_object_id,
|
|
105
|
+
"end_object_id" => r.end_object_id
|
|
106
|
+
}
|
|
107
|
+
end,
|
|
108
|
+
Ea::Qea::Models::EaDiagram => ->(r) do
|
|
109
|
+
{
|
|
110
|
+
"id" => r.diagram_id, "name" => r.name,
|
|
111
|
+
"type" => r.diagram_type, "package_id" => r.package_id
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
}.freeze
|
|
39
115
|
end
|
|
40
116
|
end
|
|
41
117
|
end
|
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string: true
|
|
2
2
|
|
|
3
3
|
module Ea
|
|
4
4
|
module Export
|
|
5
5
|
module PlantUml
|
|
6
6
|
# Generates PlantUML class diagram text from a parsed QEA model.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# Person "1" --> "0..*" Address : lives_at
|
|
15
|
-
# @enduml
|
|
8
|
+
# Emits:
|
|
9
|
+
# - Package nesting (`package "X" { ... }`)
|
|
10
|
+
# - Class declarations with attributes and operations
|
|
11
|
+
# - Generalizations (`Parent <|-- Child`)
|
|
12
|
+
# - Associations with multiplicities and role names
|
|
13
|
+
# - Enumerations as `enum` blocks
|
|
16
14
|
class Generator
|
|
17
15
|
def self.call(model, **_opts)
|
|
18
16
|
new(model).call
|
|
@@ -25,8 +23,8 @@ module Ea
|
|
|
25
23
|
end
|
|
26
24
|
|
|
27
25
|
def call
|
|
28
|
-
lines = ["@startuml"]
|
|
29
|
-
lines.concat(
|
|
26
|
+
lines = ["@startuml", "skinparam classAttributeIconSize 0"]
|
|
27
|
+
lines.concat(package_lines)
|
|
30
28
|
lines.concat(connector_lines)
|
|
31
29
|
lines << "@enduml"
|
|
32
30
|
lines.join("\n") + "\n"
|
|
@@ -34,38 +32,107 @@ module Ea
|
|
|
34
32
|
|
|
35
33
|
private
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
# Wrap classes/enums in their containing packages.
|
|
36
|
+
def package_lines
|
|
37
|
+
packages_by_id = packages_index
|
|
38
|
+
root_objects = (model.collections[:objects] || [])
|
|
39
|
+
|
|
40
|
+
# Group classes by their package_id; emit each package.
|
|
41
|
+
grouped = root_objects.group_by(&:package_id)
|
|
42
|
+
grouped.flat_map do |pkg_id, objs|
|
|
43
|
+
next ungrouped_lines(objs) if pkg_id.nil?
|
|
44
|
+
|
|
45
|
+
pkg = packages_by_id[pkg_id]
|
|
46
|
+
pkg_name = pkg&.name || "Package#{pkg_id}"
|
|
47
|
+
inner = objs.flat_map { |o| class_block(o) }
|
|
48
|
+
next inner if inner.empty?
|
|
49
|
+
|
|
50
|
+
["package \"#{pkg_name}\" {",
|
|
51
|
+
*inner.map { |line| " #{line}" },
|
|
52
|
+
"}"]
|
|
53
|
+
end.compact
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def ungrouped_lines(objs)
|
|
57
|
+
objs.flat_map { |o| class_block(o) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Emit `class X { +attr: T ... }` or `enum X { LITERAL1 ... }`.
|
|
61
|
+
def class_block(obj)
|
|
62
|
+
case obj.object_type
|
|
63
|
+
when "Class"
|
|
64
|
+
attrs = class_attributes(obj)
|
|
65
|
+
body = attrs.map { |a| "+#{a.name}: #{a.type || 'String'}" }
|
|
66
|
+
return ["class #{obj.name}"] if body.empty?
|
|
67
|
+
|
|
68
|
+
["class #{obj.name} {", *body.map { |b| " #{b}" }, "}"]
|
|
69
|
+
when "Enumeration"
|
|
70
|
+
literals = enum_literals(obj)
|
|
71
|
+
return ["enum #{obj.name}"] if literals.empty?
|
|
72
|
+
|
|
73
|
+
["enum #{obj.name} {",
|
|
74
|
+
*literals.map { |l| " #{l.name || l}" },
|
|
75
|
+
"}"]
|
|
76
|
+
when "Interface"
|
|
77
|
+
["interface #{obj.name}"]
|
|
78
|
+
when "DataType"
|
|
79
|
+
["abstract class #{obj.name}"]
|
|
80
|
+
else
|
|
81
|
+
["class #{obj.name}"]
|
|
42
82
|
end
|
|
43
83
|
end
|
|
44
84
|
|
|
85
|
+
# Generalizations: `<|`-style inheritance.
|
|
86
|
+
# Associations: multiplicity + role names.
|
|
45
87
|
def connector_lines
|
|
46
|
-
(model.collections[:connectors] || []).
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
next nil unless
|
|
88
|
+
(model.collections[:connectors] || []).filter_map do |conn|
|
|
89
|
+
src = find_object(conn.start_object_id)
|
|
90
|
+
tgt = find_object(conn.end_object_id)
|
|
91
|
+
next nil unless src && tgt
|
|
50
92
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
93
|
+
case conn.connector_type
|
|
94
|
+
when "Generalization"
|
|
95
|
+
"#{tgt.name} <|-- #{src.name}"
|
|
96
|
+
when "Association", "Aggregation", "Composition"
|
|
97
|
+
assoc_arrow(src, tgt, conn)
|
|
98
|
+
else
|
|
99
|
+
label = conn.name ? " : #{conn.name}" : ""
|
|
100
|
+
"#{src.name} --> #{tgt.name}#{label}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
54
103
|
end
|
|
55
104
|
|
|
56
|
-
def
|
|
57
|
-
|
|
58
|
-
|
|
105
|
+
def assoc_arrow(src, tgt, conn)
|
|
106
|
+
arrow = case conn.connector_type
|
|
107
|
+
when "Composition" then "*--"
|
|
108
|
+
when "Aggregation" then "o--"
|
|
109
|
+
else "--"
|
|
110
|
+
end
|
|
111
|
+
label = conn.name ? " : #{conn.name}" : ""
|
|
112
|
+
"#{src.name} #{arrow} #{tgt.name}#{label}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def packages_index
|
|
116
|
+
(model.collections[:packages] || []).each_with_object({}) do |p, acc|
|
|
117
|
+
acc[p.package_id] = p
|
|
118
|
+
end
|
|
59
119
|
end
|
|
60
120
|
|
|
61
121
|
def class_attributes(klass)
|
|
62
122
|
attrs = model.collections[:attributes] || []
|
|
63
|
-
attrs.select { |a| a.
|
|
123
|
+
attrs.select { |a| a.ea_object_id == klass.ea_object_id }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def enum_literals(enum)
|
|
127
|
+
literals = model.collections[:enumeration_literals] ||
|
|
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 }
|
|
64
131
|
end
|
|
65
132
|
|
|
66
133
|
def find_object(object_id)
|
|
67
134
|
(model.collections[:objects] || [])
|
|
68
|
-
.find { |o| o.object_id == object_id }
|
|
135
|
+
.find { |o| o.ea_object_id == object_id || o.object_id == object_id }
|
|
69
136
|
end
|
|
70
137
|
end
|
|
71
138
|
end
|
|
@@ -7,26 +7,21 @@ module Ea
|
|
|
7
7
|
module Xsd
|
|
8
8
|
# Generates an XSD schema string from a parsed QEA model.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# Both class_mapping and namespaces default to the EA-bundled
|
|
16
|
-
# GML fixtures when present, so callers can do
|
|
17
|
-
# Ea::Export::Xsd::Generator.call(model, target_namespace: ...)
|
|
18
|
-
# without manually loading XML fixtures.
|
|
10
|
+
# Pure: takes already-loaded ClassMapping and NamespaceRegistry
|
|
11
|
+
# instances. No file IO, no fixture-path coupling. The CLI
|
|
12
|
+
# layer is responsible for loading GMLClassMapping.xml and
|
|
13
|
+
# GMLNamespaces.xml from disk.
|
|
19
14
|
class Generator
|
|
20
15
|
XSD_NS = "http://www.w3.org/2001/XMLSchema"
|
|
21
16
|
|
|
22
|
-
DEFAULT_MAPPING_PATH = "spec/fixtures/mdg/ea_config/gml/GMLClassMapping.xml".freeze
|
|
23
|
-
DEFAULT_NAMESPACES_PATH = "spec/fixtures/mdg/ea_config/gml/GMLNamespaces.xml".freeze
|
|
24
|
-
|
|
25
17
|
attr_reader :class_mapping, :namespaces
|
|
26
18
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
# @param class_mapping [ClassMapping] type translation rules
|
|
20
|
+
# @param namespaces [NamespaceRegistry] namespace declarations
|
|
21
|
+
def initialize(class_mapping: ClassMapping.new,
|
|
22
|
+
namespaces: NamespaceRegistry.new)
|
|
23
|
+
@class_mapping = class_mapping
|
|
24
|
+
@namespaces = namespaces
|
|
30
25
|
end
|
|
31
26
|
|
|
32
27
|
# Class-method shortcut. Accepts the same options as #initialize
|
data/lib/ea/export.rb
CHANGED
|
@@ -5,7 +5,6 @@ module Ea
|
|
|
5
5
|
# One sub-namespace per output format; each is independent and
|
|
6
6
|
# autoloadable.
|
|
7
7
|
module Export
|
|
8
|
-
autoload :Xmi, "ea/export/xmi"
|
|
9
8
|
autoload :Json, "ea/export/json"
|
|
10
9
|
autoload :JsonSchema, "ea/export/json_schema"
|
|
11
10
|
autoload :PlantUml, "ea/export/plantuml"
|
|
@@ -3,31 +3,36 @@
|
|
|
3
3
|
module Ea
|
|
4
4
|
module Lint
|
|
5
5
|
module Rules
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# Flags classes with no applied stereotype. Stereotype
|
|
7
|
+
# detection walks t_xref for `@STEREO;Name=...;` blocks
|
|
8
|
+
# referencing the class's ea_guid.
|
|
8
9
|
class MissingStereotypeRule < LintRule
|
|
9
10
|
self.severity = :info
|
|
10
11
|
|
|
11
|
-
GML_STEREOTYPES = %w[FeatureType Type DataType CodeList
|
|
12
|
-
Enumeration ObjectType].freeze
|
|
13
|
-
|
|
14
12
|
def check(model)
|
|
15
13
|
classes = (model.collections[:objects] || [])
|
|
16
14
|
.select { |o| o.object_type == "Class" }
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
xrefs = model.collections[:xrefs] || []
|
|
16
|
+
classes.map do |klass|
|
|
17
|
+
next nil if stereotype_name_for(klass, xrefs)
|
|
18
|
+
|
|
19
19
|
offense(entity_id: klass.object_id,
|
|
20
20
|
entity_name: klass.name,
|
|
21
21
|
message: "Class has no applied stereotype",
|
|
22
22
|
severity: :info)
|
|
23
|
-
end
|
|
23
|
+
end.compact
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
private
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
# Find the first @STEREO application in t_xref that references
|
|
29
|
+
# the class's ea_guid.
|
|
30
|
+
def stereotype_name_for(klass, xrefs)
|
|
31
|
+
return nil unless klass.is_a?(Ea::Qea::Models::EaObject)
|
|
32
|
+
|
|
33
|
+
xrefs.find do |xr|
|
|
34
|
+
xr.client == klass.ea_guid && xr.description&.include?("@STEREO")
|
|
35
|
+
end&.description&.match(/Name=([^;]+)/)&.[](1)
|
|
31
36
|
end
|
|
32
37
|
end
|
|
33
38
|
end
|