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
@@ -28,22 +28,63 @@ module Ea
28
28
  OFFSET_X = 0
29
29
  OFFSET_Y = 0
30
30
 
31
- attr_reader :classifier, :bounds, :canvas
31
+ attr_reader :classifier, :bounds, :canvas, :mdg_registry
32
32
 
33
- def initialize(classifier:, bounds:, canvas: nil)
33
+ # @param mdg_registry [Ea::Mdg::Registry, nil] optional
34
+ # registry to look up MDG-provided ShapeScript for the
35
+ # classifier's stereotype. When provided and the matching
36
+ # stereotype has a ShapeScript body, it's parsed and
37
+ # rendered. Otherwise falls back to FALLBACK_ICONS.
38
+ def initialize(classifier:, bounds:, canvas: nil, mdg_registry: nil)
34
39
  @classifier = classifier
35
40
  @bounds = bounds
36
41
  @canvas = canvas
42
+ @mdg_registry = mdg_registry
37
43
  end
38
44
 
39
- def self.render(classifier:, bounds:, canvas: nil, **_)
40
- new(classifier: classifier, bounds: bounds, canvas: canvas).to_svg
45
+ def self.render(classifier:, bounds:, canvas: nil, **opts)
46
+ new(classifier: classifier, bounds: bounds, canvas: canvas, **opts).to_svg
41
47
  end
42
48
 
43
- # @return [String] SVG polygon fragment, or "" if no icon applies
49
+ # @return [String] SVG fragment, or "" if no icon applies
44
50
  def to_svg
45
51
  return "" unless classifier && bounds
52
+ return "" unless stereotype_name
46
53
 
54
+ shapescript_svg || fallback_svg
55
+ end
56
+
57
+ private
58
+
59
+ # Try MDG-provided ShapeScript first.
60
+ # @return [String, nil] SVG fragment from parsed ShapeScript
61
+ def shapescript_svg
62
+ return nil unless mdg_registry
63
+
64
+ body = lookup_shapescript(stereotype_name)
65
+ return nil unless body
66
+
67
+ shapes = Ea::Shapescript::Parser.parse(body)
68
+ return nil if shapes.empty?
69
+
70
+ Ea::Shapescript::Renderer.render(shapes, fill: "#FAF1EC",
71
+ stroke: "#69738C")
72
+ end
73
+
74
+ # @return [String, nil] ShapeScript source for the stereotype, if any
75
+ def lookup_shapescript(name)
76
+ mdg_registry.documents.each do |doc|
77
+ stereo = (doc.stereotypes || []).find { |s| s.name == name }
78
+ next unless stereo
79
+
80
+ notes = stereo.notes
81
+ return notes if notes && notes.include?("shape")
82
+ end
83
+ nil
84
+ end
85
+
86
+ # @return [String] hardcoded fallback polygon, or "" if none
87
+ def fallback_svg
47
88
  spec = FALLBACK_ICONS[stereotype_name]
48
89
  return "" unless spec
49
90
 
@@ -55,15 +96,11 @@ module Ea
55
96
  %(<polygon points="#{pts_str}" fill="#{fill}" stroke="#{stroke}" stroke-width="1"/>)
56
97
  end
57
98
 
58
- private
59
-
60
99
  def stereotype_name
61
- refs = classifier.respond_to?(:stereotype_refs) ? classifier.stereotype_refs : nil
62
- return refs.first if refs&.any?
100
+ return nil unless classifier.is_a?(Ea::Model::Classifier)
63
101
 
64
- # Walk xref description as a fallback when classifier has no
65
- # explicit stereotype_refs slot (e.g. raw Ea::Qea::Models::EaObject).
66
- nil
102
+ refs = classifier.stereotype_refs
103
+ refs&.first
67
104
  end
68
105
 
69
106
  def translate_point(x, y)
@@ -70,8 +70,144 @@ module Ea
70
70
  # nil-valued attributes are skipped at the source. No
71
71
  # post-processing pass is needed — the prior XmlSanitizer
72
72
  # workaround (TODO 21 §1) has been removed.
73
- def serialize
74
- build_root.to_xml(use_prefix: true)
73
+ # Serialize to XMI XML.
74
+ #
75
+ # Default produces model tree only (parseable by the xmi gem's
76
+ # Sparx parser for round-trip). Pass `with_extensions: true` to
77
+ # also emit EA-specific `<connectors>` and `<diagrams>` sections
78
+ # inside `<xmi:Extension>`. Those use post-processing string
79
+ # injection that the xmi gem's parser doesn't model, so the
80
+ # output is NOT round-trippable when extensions are included.
81
+ #
82
+ # @param with_extensions [Boolean]
83
+ # @return [String] XMI XML
84
+ def serialize(with_extensions: true)
85
+ xml = build_root.to_xml(use_prefix: true)
86
+ with_extensions ? inject_extension_content(xml) : xml
87
+ end
88
+
89
+ # Injects EA-specific extension content (connectors, diagrams)
90
+ # into the serialized XMI. The xmi gem's Extension type is
91
+ # modeled but doesn't declare `connectors` / `diagrams`
92
+ # children. We post-process the serialized XML to insert
93
+ # them inside the existing `<xmi:Extension>` element.
94
+ def inject_extension_content(xml)
95
+ content = extension_inner_xml
96
+ return xml if content.empty?
97
+
98
+ # Replace the empty `<xmi:Extension .../>` or the
99
+ # closing `</xmi:Extension>` with the populated version.
100
+ if xml.include?("<xmi:Extension")
101
+ xml.sub(%r{<xmi:Extension ([^>]+)>\s*</xmi:Extension>},
102
+ "<xmi:Extension \\1>\n#{content}\n\t</xmi:Extension>")
103
+ .sub(/<xmi:Extension ([^>]+)\/>/,
104
+ "<xmi:Extension \\1>\n#{content}\n\t</xmi:Extension>")
105
+ else
106
+ xml
107
+ end
108
+ end
109
+
110
+ # @return [String] XML for the inner content of <xmi:Extension>:
111
+ # <elements>...</elements>, <connectors>...</connectors>,
112
+ # <diagrams>...</diagrams>. Empty when none apply.
113
+ def extension_inner_xml
114
+ parts = []
115
+ connectors_xml = build_connectors_extension
116
+ parts << connectors_xml unless connectors_xml.empty?
117
+ diagrams_xml = build_diagrams_extension
118
+ parts << diagrams_xml unless diagrams_xml.empty?
119
+ parts.join("\n")
120
+ end
121
+
122
+ def build_connectors_extension
123
+ connectors = @database.collections[:connectors] || []
124
+ return "" if connectors.empty?
125
+
126
+ inner = connectors.map { |conn| connector_xml(conn) }.compact.join("\n")
127
+ return "" if inner.empty?
128
+
129
+ "\t\t<connectors>\n#{inner}\n\t\t</connectors>"
130
+ end
131
+
132
+ def build_diagrams_extension
133
+ diagrams = @database.collections[:diagrams] || []
134
+ return "" if diagrams.empty?
135
+
136
+ inner = diagrams.map { |dgm| diagram_xml(dgm) }.compact.join("\n")
137
+ return "" if inner.empty?
138
+
139
+ "\t\t<diagrams>\n#{inner}\n\t\t</diagrams>"
140
+ end
141
+
142
+ # Per-connector XML with xmi:idref to the model-level
143
+ # association (when present) plus source/target references.
144
+ def connector_xml(conn)
145
+ return nil unless conn.ea_guid
146
+
147
+ connector_id = "EAID_#{guid_to_xmi_id(conn.ea_guid)}"
148
+ source_obj = @database.collections[:objects]&.find { |o| o.ea_object_id == conn.start_object_id }
149
+ target_obj = @database.collections[:objects]&.find { |o| o.ea_object_id == conn.end_object_id }
150
+ return nil unless source_obj && target_obj
151
+
152
+ name_attr = conn.name ? %( name="#{escape(conn.name)}") : ""
153
+ inner = source_end_xml(conn, source_obj) + target_end_xml(conn, target_obj)
154
+ %(<connector xmi:idref="#{connector_id}"#{name_attr}>\n#{inner}\t\t</connector>)
155
+ end
156
+
157
+ def source_end_xml(conn, source_obj)
158
+ end_xml(source_obj, "source")
159
+ end
160
+
161
+ def target_end_xml(conn, target_obj)
162
+ end_xml(target_obj, "target")
163
+ end
164
+
165
+ def end_xml(obj, kind)
166
+ xmi_id = "EAID_#{guid_to_xmi_id(obj.ea_guid)}"
167
+ lines = [
168
+ "\t\t\t<#{kind} xmi:idref=\"#{xmi_id}\">",
169
+ "\t\t\t\t<model ea_localid=\"#{obj.ea_object_id}\" " \
170
+ "type=\"#{obj.object_type}\" name=\"#{escape(obj.name.to_s)}\"/>",
171
+ "\t\t\t\t<role visibility=\"Public\" targetScope=\"instance\"/>",
172
+ "\t\t\t</#{kind}>"
173
+ ]
174
+ lines.join("\n") + "\n"
175
+ end
176
+
177
+ def diagram_xml(dgm)
178
+ return nil unless dgm.ea_guid
179
+
180
+ diagram_id = "EAID_#{guid_to_xmi_id(dgm.ea_guid)}"
181
+ pkg_guid = package_guid_for(dgm)
182
+ pkg_ref = pkg_guid ? "EAPK_#{guid_to_xmi_id(pkg_guid)}" : ""
183
+ lines = [
184
+ "<diagram xmi:id=\"#{diagram_id}\">",
185
+ "\t\t\t<model package=\"#{pkg_ref}\" localID=\"#{dgm.diagram_id}\" owner=\"#{pkg_ref}\"/>",
186
+ "\t\t\t<properties name=\"#{escape(dgm.name.to_s)}\" type=\"#{escape(dgm.diagram_type.to_s)}\"/>",
187
+ "\t\t</diagram>"
188
+ ]
189
+ lines.join("\n")
190
+ end
191
+
192
+ def package_guid_for(dgm)
193
+ pkg = (@database.collections[:packages] || []).find { |p| p.package_id == dgm.package_id }
194
+ pkg&.ea_guid || dgm.ea_guid
195
+ end
196
+
197
+ def guid_to_xmi_id(guid)
198
+ return "" unless guid
199
+
200
+ guid.to_s.gsub(/[{}]/, "")
201
+ .tr("-", "_")
202
+ .prepend("EA") # truncated below
203
+ .sub(/\AEA/, "")
204
+ end
205
+
206
+ def escape(text)
207
+ text.to_s.gsub("&", "&amp;")
208
+ .gsub("<", "&lt;")
209
+ .gsub(">", "&gt;")
210
+ .gsub('"', "&quot;")
75
211
  end
76
212
 
77
213
  private
@@ -25,9 +25,10 @@ module Ea
25
25
  # Sparx-specific concepts (stereotypes, tagged values, multiplicities,
26
26
  # diagrams, xrefs).
27
27
  # @param database [Ea::Qea::Database]
28
+ # @param with_extensions [Boolean] emit EA-specific connectors+diagrams
28
29
  # @return [String] XMI XML
29
- def qea_to_xmi(database)
30
- QeaToXmi::Transformer.new(database).serialize
30
+ def qea_to_xmi(database, with_extensions: true)
31
+ QeaToXmi::Transformer.new(database).serialize(with_extensions: with_extensions)
31
32
  end
32
33
  end
33
34
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Validation
5
+ # Compares Ea::Transformers.qea_to_xmi output against EA's
6
+ # reference XMI export (`examples/exports/*/model.xml`).
7
+ #
8
+ # Reports per-element-type counts (packagedElement, ownedAttribute,
9
+ # ownedEnd, memberEnd, generalization, connector, diagram, style,
10
+ # tags, documentation, xmi:Extension) so gaps surface clearly.
11
+ #
12
+ # Used by the parity regression spec to track XMI export fidelity
13
+ # over time. Improvements to QeaToXmi should shrink the gaps.
14
+ class XmiParity
15
+ ELEMENT_TYPES = %w[
16
+ packagedElement
17
+ ownedAttribute
18
+ ownedOperation
19
+ ownedEnd
20
+ memberEnd
21
+ generalization
22
+ connector
23
+ diagram
24
+ taggedValue
25
+ xmi:Extension
26
+ style
27
+ tags
28
+ documentation
29
+ ].freeze
30
+
31
+ # Per-element counts for one XMI document.
32
+ Counts = Struct.new(:total, :by_type, keyword_init: true) do
33
+ def initialize(*)
34
+ super
35
+ self.by_type ||= {}
36
+ end
37
+
38
+ def delta(other)
39
+ result = { total: total - other.total }
40
+ ELEMENT_TYPES.each do |t|
41
+ result[t] = (by_type[t] || 0) - (other.by_type[t] || 0)
42
+ end
43
+ result
44
+ end
45
+ end
46
+
47
+ # @param qea_path [String] path to the QEA file
48
+ # @param reference_xmi_path [String] path to EA's reference XMI
49
+ # @return [Hash] {:ours, :reference, :delta} each a Counts
50
+ def self.compare(qea_path, reference_xmi_path)
51
+ database = Ea::Qea.load(qea_path)
52
+ ours_xml = Ea::Transformers.qea_to_xmi(database)
53
+ ref_xml = File.read(reference_xmi_path)
54
+
55
+ {
56
+ qea: qea_path,
57
+ reference: reference_xmi_path,
58
+ ours: count(ours_xml),
59
+ reference_counts: count(ref_xml),
60
+ delta: count(ours_xml).delta(count(ref_xml))
61
+ }
62
+ end
63
+
64
+ # @param xml [String] XMI markup (any encoding)
65
+ # @return [Counts]
66
+ def self.count(xml)
67
+ # EA exports use windows-1252; force UTF-8 for safe regex.
68
+ safe = xml.to_s.encode("UTF-8", invalid: :replace, undef: :replace,
69
+ replace: "?")
70
+ by_type = ELEMENT_TYPES.each_with_object({}) do |tag, acc|
71
+ # Match `<tag>` followed by space, `/`, or `>` (covers
72
+ # `<tag/>`, `<tag foo=...>`, `<tag>`). Using %r{} to avoid
73
+ # `/` regex-delimiter conflict with the `/` in the class.
74
+ acc[tag] = safe.scan(%r{<#{tag}[\s/>]}).size
75
+ end
76
+ Counts.new(total: by_type.values.sum, by_type: by_type)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string: true
2
+
3
+ module Ea
4
+ # Top-level validation namespace. Hosts cross-cutting validators
5
+ # that span multiple subsystems (QEA + XMI, lint rules, OCL).
6
+ #
7
+ # Distinct from Ea::Qea::Validation which is specific to QEA
8
+ # database + UML document structural validation.
9
+ module Validation
10
+ autoload :XmiParity, "ea/validation/xmi_parity"
11
+ end
12
+ end
data/lib/ea/version.rb CHANGED
@@ -4,5 +4,5 @@ module Ea
4
4
  # Gem release version. Extracted to its own file so the
5
5
  # `gem-release` tool's `gem bump` can find and update it via the
6
6
  # standard `lib/<gem>/version.rb` convention.
7
- VERSION = "0.5.0"
7
+ VERSION = "0.5.2"
8
8
  end
data/lib/ea.rb CHANGED
@@ -29,6 +29,7 @@ module Ea
29
29
  autoload :Lint, "ea/lint"
30
30
  autoload :Query, "ea/query"
31
31
  autoload :Ocl, "ea/ocl"
32
+ autoload :Validation, "ea/validation"
32
33
 
33
34
  class << self
34
35
  # Parse an EA file into its native representation.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ea
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -223,6 +223,25 @@ files:
223
223
  - TODO.complete/45-shapescript-extensions.md
224
224
  - TODO.complete/46-xsd-consolidation.md
225
225
  - TODO.complete/47-headerlines-legacy-removal.md
226
+ - TODO.complete/51-remove-respond-to.md
227
+ - TODO.complete/52-xsd-decouple-fixtures.md
228
+ - TODO.complete/53-xmi-public-send-cleanup.md
229
+ - TODO.complete/54-wire-stereotype-icon-renderer.md
230
+ - TODO.complete/55-wire-shapescript.md
231
+ - TODO.complete/56-wire-emf-renderer.md
232
+ - TODO.complete/57-wire-ocl-evaluator.md
233
+ - TODO.complete/58-diff-modifications.md
234
+ - TODO.complete/59-delete-toy-xmi-export.md
235
+ - TODO.complete/60-curate-json-export.md
236
+ - TODO.complete/61-plantuml-relationships.md
237
+ - TODO.complete/62-changelog.md
238
+ - TODO.complete/63-dependabot-fix.md
239
+ - TODO.complete/64-qeatoxmi-connectors.md
240
+ - TODO.complete/65-qeatoxmi-diagrams.md
241
+ - TODO.complete/66-qeatoxmi-style-tags-docs.md
242
+ - TODO.complete/67-ocl-collection-ops.md
243
+ - TODO.complete/68-ocl-comparison-ops.md
244
+ - TODO.complete/69-spec-new-models.md
226
245
  - TODO.complete/STATUS.md
227
246
  - TODO.diagrams/01-svg-measurement-harness.md
228
247
  - TODO.diagrams/01-xmi-extension-elements-parsing.md
@@ -586,8 +605,6 @@ files:
586
605
  - lib/ea/export/json_schema/generator.rb
587
606
  - lib/ea/export/plantuml.rb
588
607
  - lib/ea/export/plantuml/generator.rb
589
- - lib/ea/export/xmi.rb
590
- - lib/ea/export/xmi/generator.rb
591
608
  - lib/ea/export/xsd.rb
592
609
  - lib/ea/export/xsd/class_mapping.rb
593
610
  - lib/ea/export/xsd/generator.rb
@@ -753,6 +770,7 @@ files:
753
770
  - lib/ea/qea/validation/class_validator.rb
754
771
  - lib/ea/qea/validation/database.rb
755
772
  - lib/ea/qea/validation/database/circular_reference_validator.rb
773
+ - lib/ea/qea/validation/database/ocl_constraint_validator.rb
756
774
  - lib/ea/qea/validation/database/orphan_validator.rb
757
775
  - lib/ea/qea/validation/database/referential_integrity_validator.rb
758
776
  - lib/ea/qea/validation/diagram_validator.rb
@@ -862,6 +880,7 @@ files:
862
880
  - lib/ea/svg/ea_emitter/compartment/package_contents.rb
863
881
  - lib/ea/svg/ea_emitter/compartment/package_from_parent.rb
864
882
  - lib/ea/svg/ea_emitter/compartment/shape.rb
883
+ - lib/ea/svg/ea_emitter/compartment/stereotype_icon.rb
865
884
  - lib/ea/svg/ea_emitter/compartment/tagged_values.rb
866
885
  - lib/ea/svg/ea_emitter/connectors.rb
867
886
  - lib/ea/svg/ea_emitter/diagram_frame.rb
@@ -940,6 +959,8 @@ files:
940
959
  - lib/ea/transformers/uml_to_xmi/id_generator.rb
941
960
  - lib/ea/transformers/uml_to_xmi/transformer.rb
942
961
  - lib/ea/transformers/uml_to_xmi/writer.rb
962
+ - lib/ea/validation.rb
963
+ - lib/ea/validation/xmi_parity.rb
943
964
  - lib/ea/version.rb
944
965
  - lib/ea/xmi.rb
945
966
  - lib/ea/xmi/liquid_drops/association_drop.rb
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "nokogiri"
4
-
5
- module Ea
6
- module Export
7
- module Xmi
8
- # Generates Sparx-flavored XMI 2.1 from a parsed QEA model.
9
- #
10
- # Uses the existing QEA→XMI transformer under Ea::Transformers
11
- # when available. Falls back to a minimal hand-rolled writer
12
- # that produces a valid XMI root + class list when the bridge
13
- # is unavailable.
14
- class Generator
15
- def self.call(model, **_opts)
16
- new(model).call
17
- end
18
-
19
- attr_reader :model
20
-
21
- # @param model [#collections] Ea::Qea::Database or compatible
22
- def initialize(model)
23
- @model = model
24
- end
25
-
26
- def call
27
- builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
28
- xml.xmi("XMI" => "2.1",
29
- "xmlns:UML" => "http://schema.omg.org/spec/UML/2.1",
30
- "xmlns:xmi" => "http://schema.omg.org/spec/XMI/2.1") do
31
- xml.documentation(:exporter => "ea-rb",
32
- :exporterVersion => Ea::VERSION)
33
- emit_model(xml)
34
- end
35
- end
36
- builder.to_xml
37
- end
38
-
39
- private
40
-
41
- def emit_model(xml)
42
- classes = (model.collections[:objects] || [])
43
- .select { |o| o.object_type == "Class" }
44
-
45
- xml.tag!("uml:Model", "xmi:id" => "model-root",
46
- "name" => "model") do
47
- classes.each do |klass|
48
- emit_class(xml, klass)
49
- end
50
- end
51
- end
52
-
53
- def emit_class(xml, klass)
54
- attrs = class_attributes(klass)
55
- xml.tag!("ownedMember",
56
- "xmi:type" => "uml:Class",
57
- "xmi:id" => klass.ea_guid || klass.object_id.to_s,
58
- "name" => klass.name || "Anonymous") do
59
- attrs.each { |attr| emit_attribute(xml, attr) }
60
- end
61
- end
62
-
63
- def emit_attribute(xml, attr)
64
- xml.tag!("ownedAttribute",
65
- "xmi:type" => "uml:Property",
66
- "xmi:id" => attr.ea_guid || attr.property_id.to_s,
67
- "name" => attr.name)
68
- end
69
-
70
- def class_attributes(klass)
71
- attrs = model.collections[:attributes] || []
72
- attrs.select { |a| a.object_id == klass.object_id }
73
- end
74
- end
75
- end
76
- end
77
- end
data/lib/ea/export/xmi.rb DELETED
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ea
4
- module Export
5
- # XMI export namespace. Generates Sparx-flavored XMI from a
6
- # parsed model.
7
- module Xmi
8
- autoload :Generator, "ea/export/xmi/generator"
9
- end
10
- end
11
- end