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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -1
  3. data/TODO.complete/51-remove-respond-to.md +25 -0
  4. data/TODO.complete/52-xsd-decouple-fixtures.md +23 -0
  5. data/TODO.complete/53-xmi-public-send-cleanup.md +26 -0
  6. data/TODO.complete/54-wire-stereotype-icon-renderer.md +20 -0
  7. data/TODO.complete/55-wire-shapescript.md +20 -0
  8. data/TODO.complete/56-wire-emf-renderer.md +27 -0
  9. data/TODO.complete/57-wire-ocl-evaluator.md +23 -0
  10. data/TODO.complete/58-diff-modifications.md +21 -0
  11. data/TODO.complete/59-delete-toy-xmi-export.md +30 -0
  12. data/TODO.complete/60-curate-json-export.md +20 -0
  13. data/TODO.complete/61-plantuml-relationships.md +22 -0
  14. data/TODO.complete/62-changelog.md +17 -0
  15. data/TODO.complete/63-dependabot-fix.md +18 -0
  16. data/TODO.complete/64-qeatoxmi-connectors.md +30 -0
  17. data/TODO.complete/65-qeatoxmi-diagrams.md +29 -0
  18. data/TODO.complete/66-qeatoxmi-style-tags-docs.md +28 -0
  19. data/TODO.complete/67-ocl-collection-ops.md +20 -0
  20. data/TODO.complete/68-ocl-comparison-ops.md +19 -0
  21. data/TODO.complete/69-spec-new-models.md +25 -0
  22. data/TODO.complete/STATUS.md +40 -64
  23. data/lib/ea/cli/command/convert.rb +1 -1
  24. data/lib/ea/cli/command/export.rb +23 -2
  25. data/lib/ea/diff/comparator.rb +28 -1
  26. data/lib/ea/export/json/generator.rb +84 -8
  27. data/lib/ea/export/plantuml/generator.rb +95 -28
  28. data/lib/ea/export/xsd/generator.rb +10 -15
  29. data/lib/ea/export.rb +0 -1
  30. data/lib/ea/lint/rules/missing_stereotype.rb +16 -11
  31. data/lib/ea/ocl/evaluator.rb +39 -2
  32. data/lib/ea/ocl/nodes.rb +9 -0
  33. data/lib/ea/ocl/parser.rb +60 -6
  34. data/lib/ea/qea/models/ea_implement.rb +2 -2
  35. data/lib/ea/qea/models/ea_object_effort.rb +2 -2
  36. data/lib/ea/qea/models/ea_object_problem.rb +2 -2
  37. data/lib/ea/qea/models/ea_object_require.rb +2 -2
  38. data/lib/ea/qea/models/ea_object_resource.rb +2 -2
  39. data/lib/ea/qea/models/ea_object_risk.rb +2 -2
  40. data/lib/ea/qea/models/ea_object_scenario.rb +2 -2
  41. data/lib/ea/qea/models/ea_object_test.rb +2 -2
  42. data/lib/ea/qea/models/ea_object_trx.rb +2 -2
  43. data/lib/ea/qea/models/ea_palette_item.rb +2 -2
  44. data/lib/ea/qea/models/ea_role_constraint.rb +2 -2
  45. data/lib/ea/qea/models/ea_secrypt.rb +2 -2
  46. data/lib/ea/qea/validation/database/ocl_constraint_validator.rb +65 -0
  47. data/lib/ea/qea/validation/database.rb +2 -0
  48. data/lib/ea/qea/validation/validation_engine.rb +2 -0
  49. data/lib/ea/qea/validation.rb +2 -0
  50. data/lib/ea/query/builder.rb +22 -3
  51. data/lib/ea/svg/ea_emitter/compartment/stereotype_icon.rb +32 -0
  52. data/lib/ea/svg/ea_emitter/compartment.rb +2 -0
  53. data/lib/ea/svg/ea_emitter/document.rb +33 -0
  54. data/lib/ea/svg/ea_emitter/element/stereotype_icon_renderer.rb +49 -12
  55. data/lib/ea/transformers/qea_to_xmi/transformer.rb +138 -2
  56. data/lib/ea/transformers.rb +3 -2
  57. data/lib/ea/validation/xmi_parity.rb +80 -0
  58. data/lib/ea/validation.rb +12 -0
  59. data/lib/ea/version.rb +1 -1
  60. data/lib/ea.rb +1 -0
  61. metadata +24 -3
  62. data/lib/ea/export/xmi/generator.rb +0 -77
  63. data/lib/ea/export/xmi.rb +0 -11
@@ -6,86 +6,62 @@ gem runs anywhere Ruby runs.
6
6
 
7
7
  ## Current state (2026-08-03)
8
8
 
9
- - **Parsing**: 43/96 QEA tables covered (glossary, lists, versions, phase,
10
- authors, image, secrypt, palette, paletteitem, implement, roleconstraint,
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, JSON, JSON Schema, PlantUML, XSD.
18
- - **ShapeScript**: parser + renderer with variables, arithmetic,
19
- conditionals, subshapes, labels.
20
- - **OCL evaluator**: invariant subset (exists, forAll, matches, and/or/not).
21
- - **Lint**: 5 rules (naming, orphan, duplicate, cyclic, missing-stereotype).
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 + lazy emfsvg adapter.
24
- - **Render pipeline**: SVG default, PNG/PDF via rsvg/ImageMagick.
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**: 1974 examples, 0 failures, 55 pending.
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
- ## Out of scope (will not implement)
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
- | 30 | [30-headerlines-ocp-refactor.md](30-headerlines-ocp-refactor.md) | HeaderLines provider chain | **completed** |
83
- | 31 | [31-public-send-cleanup.md](31-public-send-cleanup.md) | Eliminate public_send | **completed** |
84
- | 32 | [32-spec-coverage.md](32-spec-coverage.md) | Specs for new code | **completed** |
85
- | 33 | [33-blocked-parity-gaps.md](33-blocked-parity-gaps.md) | Blocked gaps documented | **completed** |
86
- | 40 | [40-ocl-evaluator.md](40-ocl-evaluator.md) | OCL evaluator | **completed** |
87
- | 41 | [41-round-trip-spec.md](41-round-trip-spec.md) | Round-trip stability | **completed** |
88
- | 47 | [47-headerlines-legacy-removal.md](47-headerlines-legacy-removal.md) | HeaderLines alias | **completed** |
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
@@ -48,7 +48,7 @@ module Ea
48
48
 
49
49
  def qea_to_xmi_xml
50
50
  database = load_database(file_path)
51
- Ea::Transformers.qea_to_xmi(database)
51
+ Ea::Transformers.qea_to_xmi(database, with_extensions: false)
52
52
  ensure
53
53
  database&.close_connection
54
54
  end
@@ -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, **o) { Ea::Export::Xmi::Generator.call(model, **o) },
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]
@@ -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
- # Walks each collection and emits array entries with the row's
10
- # lutaml-model hash representation.
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| record_hash(r) }
42
+ acc[name.to_s] = records.map { |r| project(r) }
31
43
  end
32
44
  end
33
45
 
34
- def record_hash(record)
35
- hash = record.to_hash
36
- hash["record_type"] = record.class.name.split("::").last
37
- hash
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
- # frozen_string_literal: true
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
- # Example output:
9
- # @startuml
10
- # class Person {
11
- # +name: String
12
- # +age: Integer
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(class_lines)
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
- def class_lines
38
- classes.map do |klass|
39
- attrs = class_attributes(klass)
40
- body = attrs.map { |a| " +#{a.name}: #{a.type || 'String'}" }
41
- ["class #{klass.name} {", *body, "}"].join("\n")
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] || []).map do |conn|
47
- source_obj = find_object(conn.start_object_id)
48
- target_obj = find_object(conn.end_object_id)
49
- next nil unless source_obj && target_obj
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
- label = conn.name ? " : #{conn.name}" : ""
52
- %Q{#{source_obj.name} --> #{target_obj.name}#{label}}
53
- end.compact
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 classes
57
- (model.collections[:objects] || [])
58
- .select { |o| o.object_type == "Class" }
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.object_id == klass.object_id }
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
- # Uses ClassMapping to translate UML class names to GML types
11
- # and NamespaceRegistry to declare target namespaces. Walks the
12
- # model's classifiers and emits `<xs:element>` + `<xs:complexType>`
13
- # pairs.
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
- def initialize(class_mapping: nil, namespaces: nil)
28
- @class_mapping = class_mapping || ClassMapping.from_path(DEFAULT_MAPPING_PATH)
29
- @namespaces = namespaces || NamespaceRegistry.from_path(DEFAULT_NAMESPACES_PATH)
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
- # Packages named "Application Schema" or with `applicationSchema`
7
- # stereotype should have at least one class with a GML stereotype.
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
- classes_without_stereo = classes.select { |c| stereotype_refs(c).empty? }
18
- classes_without_stereo.map do |klass|
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
- def stereotype_refs(klass)
29
- refs = klass.respond_to?(:stereotype_refs) ? klass.stereotype_refs : nil
30
- refs || []
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