ea 0.5.0 → 0.5.1
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/STATUS.md +40 -64
- 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 +2 -2
- 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/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 +18 -3
- data/lib/ea/export/xmi/generator.rb +0 -77
- data/lib/ea/export/xmi.rb +0 -11
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Qea
|
|
5
|
+
module Validation
|
|
6
|
+
# Evaluates OCL invariants stored in t_objectconstraint.
|
|
7
|
+
# For each constraint, parse its OCL expression via
|
|
8
|
+
# Ea::Ocl::Parser and evaluate it against the owning
|
|
9
|
+
# object's attribute values. Failures surface as :warning
|
|
10
|
+
# messages — OCL failures aren't structural errors.
|
|
11
|
+
class OclConstraintValidator < BaseValidator
|
|
12
|
+
def validate
|
|
13
|
+
constraints = database.collections[:object_constraints] || []
|
|
14
|
+
constraints.each { |c| check_constraint(c) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def check_constraint(constraint)
|
|
20
|
+
owner = owner_for(constraint)
|
|
21
|
+
return unless owner
|
|
22
|
+
|
|
23
|
+
ast = parse(constraint.constraint)
|
|
24
|
+
return unless ast
|
|
25
|
+
|
|
26
|
+
if evaluate(ast, owner)
|
|
27
|
+
result.add(:info, "OCL constraint '#{constraint.constraint_type}' " \
|
|
28
|
+
"passed for #{owner.name}",
|
|
29
|
+
entity_name: owner.name)
|
|
30
|
+
else
|
|
31
|
+
result.add(:warning, "OCL constraint '#{constraint.constraint_type}' " \
|
|
32
|
+
"failed for #{owner.name}: #{constraint.constraint}",
|
|
33
|
+
entity_name: owner.name)
|
|
34
|
+
end
|
|
35
|
+
rescue Ea::Ocl::UnsupportedError => e
|
|
36
|
+
result.add(:info, "Skipping OCL constraint on #{owner&.name} " \
|
|
37
|
+
"(unsupported syntax): #{e.message}",
|
|
38
|
+
entity_name: owner&.name)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Parse the OCL body. Returns nil for empty / non-invariant
|
|
42
|
+
# constraints (we don't try to evaluate non-invariant forms).
|
|
43
|
+
def parse(body)
|
|
44
|
+
return nil if body.nil? || body.empty?
|
|
45
|
+
|
|
46
|
+
Ea::Ocl::Parser.parse(body)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def evaluate(ast, owner)
|
|
50
|
+
Ea::Ocl::Evaluator.evaluate(ast, owner)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def owner_for(constraint)
|
|
54
|
+
object_id = constraint.respond_to?(:ea_object_id) ?
|
|
55
|
+
constraint.ea_object_id :
|
|
56
|
+
constraint.object_id
|
|
57
|
+
return nil unless object_id
|
|
58
|
+
|
|
59
|
+
(database.collections[:objects] || [])
|
|
60
|
+
.find { |o| o.object_id == object_id }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -10,6 +10,8 @@ module Ea
|
|
|
10
10
|
"ea/qea/validation/database/orphan_validator"
|
|
11
11
|
autoload :ReferentialIntegrityValidator,
|
|
12
12
|
"ea/qea/validation/database/referential_integrity_validator"
|
|
13
|
+
autoload :OclConstraintValidator,
|
|
14
|
+
"ea/qea/validation/database/ocl_constraint_validator"
|
|
13
15
|
end
|
|
14
16
|
end
|
|
15
17
|
end
|
|
@@ -83,6 +83,7 @@ module Ea
|
|
|
83
83
|
orphan
|
|
84
84
|
circular_reference
|
|
85
85
|
package
|
|
86
|
+
ocl_constraint
|
|
86
87
|
]
|
|
87
88
|
|
|
88
89
|
validator_names = if validators
|
|
@@ -213,6 +214,7 @@ module Ea
|
|
|
213
214
|
@registry.register(:circular_reference,
|
|
214
215
|
CircularReferenceValidator)
|
|
215
216
|
@registry.register(:package, PackageValidator)
|
|
217
|
+
@registry.register(:ocl_constraint, OclConstraintValidator)
|
|
216
218
|
|
|
217
219
|
# Phase 2: UML Tree Structure Validators
|
|
218
220
|
# DocumentStructureValidator lives in lutaml-uml and may not be
|
data/lib/ea/qea/validation.rb
CHANGED
|
@@ -20,6 +20,8 @@ module Ea
|
|
|
20
20
|
"ea/qea/validation/database/orphan_validator"
|
|
21
21
|
autoload :CircularReferenceValidator,
|
|
22
22
|
"ea/qea/validation/database/circular_reference_validator"
|
|
23
|
+
autoload :OclConstraintValidator,
|
|
24
|
+
"ea/qea/validation/database/ocl_constraint_validator"
|
|
23
25
|
autoload :Database, "ea/qea/validation/database"
|
|
24
26
|
autoload :Formatters, "ea/qea/validation/formatters"
|
|
25
27
|
autoload :ValidationEngine, "ea/qea/validation/validation_engine"
|
data/lib/ea/query/builder.rb
CHANGED
|
@@ -58,13 +58,15 @@ module Ea
|
|
|
58
58
|
filter_by { |o| o.package_id == pkg.package_id }
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
# Filter by applied stereotype name.
|
|
61
|
+
# Filter by applied stereotype name. Stereotype lookup walks
|
|
62
|
+
# t_xref for @STEREO blocks referencing the object's ea_guid.
|
|
62
63
|
# @param name [String]
|
|
63
64
|
# @return [Builder]
|
|
64
65
|
def with_stereotype(name)
|
|
66
|
+
xrefs = model.collections[:xrefs] || []
|
|
65
67
|
filter_by do |o|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
applied = stereotype_names_for(o, xrefs)
|
|
69
|
+
applied.any? { |r| r == name }
|
|
68
70
|
end
|
|
69
71
|
end
|
|
70
72
|
|
|
@@ -90,6 +92,23 @@ module Ea
|
|
|
90
92
|
Builder.new(model, filters: filters + [block], kind: @kind)
|
|
91
93
|
end
|
|
92
94
|
|
|
95
|
+
# Extract stereotype names applied to an object via t_xref.
|
|
96
|
+
# Returns [] when no @STEREO block references the object's GUID.
|
|
97
|
+
# @param object [Ea::Qea::Models::EaObject]
|
|
98
|
+
# @param xrefs [Array<Ea::Qea::Models::EaXref>]
|
|
99
|
+
# @return [Array<String>]
|
|
100
|
+
def stereotype_names_for(object, xrefs)
|
|
101
|
+
return [] unless object.is_a?(Ea::Qea::Models::EaObject)
|
|
102
|
+
return [] unless object.ea_guid
|
|
103
|
+
|
|
104
|
+
xrefs.select do |xr|
|
|
105
|
+
xr.client == object.ea_guid && xr.description&.include?("@STEREO")
|
|
106
|
+
end.map do |xr|
|
|
107
|
+
xr.description.match(/Name=([^;]+)/)[1]
|
|
108
|
+
end.compact
|
|
109
|
+
end
|
|
110
|
+
private :stereotype_names_for
|
|
111
|
+
|
|
93
112
|
# Materialize the filtered collection.
|
|
94
113
|
# @return [Array]
|
|
95
114
|
def call
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Svg
|
|
5
|
+
module EaEmitter
|
|
6
|
+
module Compartment
|
|
7
|
+
# Stereotype decorator icon compartment. Emits the small
|
|
8
|
+
# symbolic polygon that EA renders inside an element's body
|
|
9
|
+
# when a stereotype provides a custom icon (FeatureType
|
|
10
|
+
# diamond, Type polygon, etc.). Delegates to
|
|
11
|
+
# Element::StereotypeIconRenderer.
|
|
12
|
+
#
|
|
13
|
+
# Rendered after the header so the icon sits inside the
|
|
14
|
+
# element body below the stereotype label.
|
|
15
|
+
module StereotypeIcon
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def render(context)
|
|
19
|
+
return nil unless context.classifier
|
|
20
|
+
|
|
21
|
+
svg = Element::StereotypeIconRenderer.render(
|
|
22
|
+
classifier: context.classifier,
|
|
23
|
+
bounds: context.bounds,
|
|
24
|
+
canvas: context.canvas
|
|
25
|
+
)
|
|
26
|
+
svg.empty? ? nil : svg
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -14,6 +14,7 @@ module Ea
|
|
|
14
14
|
autoload :NoteBody, "ea/svg/ea_emitter/compartment/note_body"
|
|
15
15
|
autoload :Header, "ea/svg/ea_emitter/compartment/header"
|
|
16
16
|
autoload :HeaderDivider, "ea/svg/ea_emitter/compartment/header_divider"
|
|
17
|
+
autoload :StereotypeIcon, "ea/svg/ea_emitter/compartment/stereotype_icon"
|
|
17
18
|
autoload :Attributes, "ea/svg/ea_emitter/compartment/attributes"
|
|
18
19
|
autoload :Operations, "ea/svg/ea_emitter/compartment/operations"
|
|
19
20
|
autoload :EnumLiterals, "ea/svg/ea_emitter/compartment/enum_literals"
|
|
@@ -32,6 +33,7 @@ module Ea
|
|
|
32
33
|
NoteBody,
|
|
33
34
|
Header,
|
|
34
35
|
HeaderDivider,
|
|
36
|
+
StereotypeIcon,
|
|
35
37
|
Attributes,
|
|
36
38
|
Operations,
|
|
37
39
|
EnumLiterals,
|
|
@@ -26,9 +26,42 @@ module Ea
|
|
|
26
26
|
document: document)
|
|
27
27
|
.layers
|
|
28
28
|
.reject { |s| s.nil? || s.empty? }
|
|
29
|
+
image_layer = emit_images
|
|
30
|
+
layers << image_layer if image_layer
|
|
29
31
|
|
|
30
32
|
%(<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">\n\n<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="#{canvas.width_cm}" height="#{canvas.height_cm}" viewBox="#{canvas.view_box}">\n<title></title>\n<desc>Created with Enterprise Architect (Build: #{BUILD_ID}) 2</desc>\n#{layers.join("\n")}\n</svg>)
|
|
31
33
|
end
|
|
34
|
+
|
|
35
|
+
# Walks t_image rows (when the document carries a Database
|
|
36
|
+
# with an :images collection) and invokes EmfRenderer on
|
|
37
|
+
# each. Emits an `<image>` element per successfully converted
|
|
38
|
+
# SVG, or returns nil when no images or all conversions fail.
|
|
39
|
+
#
|
|
40
|
+
# The emfsvg gem currently can't parse EA's specific EMF
|
|
41
|
+
# variant (see TODO.diagrams/87 + TODO.complete/56). Until
|
|
42
|
+
# upstream emfsvg supports it, this method returns nil
|
|
43
|
+
# gracefully. The wiring is correct infrastructure for when
|
|
44
|
+
# emfsvg catches up.
|
|
45
|
+
def emit_images
|
|
46
|
+
return nil unless document
|
|
47
|
+
return nil unless document.is_a?(Ea::Qea::Database)
|
|
48
|
+
|
|
49
|
+
images = document.collections[:images] || []
|
|
50
|
+
return nil if images.empty?
|
|
51
|
+
|
|
52
|
+
fragments = images.filter_map { |image| render_image(image) }
|
|
53
|
+
return nil if fragments.empty?
|
|
54
|
+
|
|
55
|
+
%(<g id="images">\n#{fragments.join("\n")}\n</g>)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def render_image(image)
|
|
59
|
+
svg = Ea::Image::EmfRenderer.render(image.bytes)
|
|
60
|
+
return nil unless svg
|
|
61
|
+
|
|
62
|
+
%(<g>#{svg}</g>)
|
|
63
|
+
end
|
|
64
|
+
private :render_image
|
|
32
65
|
end
|
|
33
66
|
end
|
|
34
67
|
end
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
62
|
-
return refs.first if refs&.any?
|
|
100
|
+
return nil unless classifier.is_a?(Ea::Model::Classifier)
|
|
63
101
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
nil
|
|
102
|
+
refs = classifier.stereotype_refs
|
|
103
|
+
refs&.first
|
|
67
104
|
end
|
|
68
105
|
|
|
69
106
|
def translate_point(x, y)
|
|
@@ -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
data/lib/ea.rb
CHANGED
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.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -223,6 +223,19 @@ 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
|
|
226
239
|
- TODO.complete/STATUS.md
|
|
227
240
|
- TODO.diagrams/01-svg-measurement-harness.md
|
|
228
241
|
- TODO.diagrams/01-xmi-extension-elements-parsing.md
|
|
@@ -586,8 +599,6 @@ files:
|
|
|
586
599
|
- lib/ea/export/json_schema/generator.rb
|
|
587
600
|
- lib/ea/export/plantuml.rb
|
|
588
601
|
- lib/ea/export/plantuml/generator.rb
|
|
589
|
-
- lib/ea/export/xmi.rb
|
|
590
|
-
- lib/ea/export/xmi/generator.rb
|
|
591
602
|
- lib/ea/export/xsd.rb
|
|
592
603
|
- lib/ea/export/xsd/class_mapping.rb
|
|
593
604
|
- lib/ea/export/xsd/generator.rb
|
|
@@ -753,6 +764,7 @@ files:
|
|
|
753
764
|
- lib/ea/qea/validation/class_validator.rb
|
|
754
765
|
- lib/ea/qea/validation/database.rb
|
|
755
766
|
- lib/ea/qea/validation/database/circular_reference_validator.rb
|
|
767
|
+
- lib/ea/qea/validation/database/ocl_constraint_validator.rb
|
|
756
768
|
- lib/ea/qea/validation/database/orphan_validator.rb
|
|
757
769
|
- lib/ea/qea/validation/database/referential_integrity_validator.rb
|
|
758
770
|
- lib/ea/qea/validation/diagram_validator.rb
|
|
@@ -862,6 +874,7 @@ files:
|
|
|
862
874
|
- lib/ea/svg/ea_emitter/compartment/package_contents.rb
|
|
863
875
|
- lib/ea/svg/ea_emitter/compartment/package_from_parent.rb
|
|
864
876
|
- lib/ea/svg/ea_emitter/compartment/shape.rb
|
|
877
|
+
- lib/ea/svg/ea_emitter/compartment/stereotype_icon.rb
|
|
865
878
|
- lib/ea/svg/ea_emitter/compartment/tagged_values.rb
|
|
866
879
|
- lib/ea/svg/ea_emitter/connectors.rb
|
|
867
880
|
- lib/ea/svg/ea_emitter/diagram_frame.rb
|
|
@@ -940,6 +953,8 @@ files:
|
|
|
940
953
|
- lib/ea/transformers/uml_to_xmi/id_generator.rb
|
|
941
954
|
- lib/ea/transformers/uml_to_xmi/transformer.rb
|
|
942
955
|
- lib/ea/transformers/uml_to_xmi/writer.rb
|
|
956
|
+
- lib/ea/validation.rb
|
|
957
|
+
- lib/ea/validation/xmi_parity.rb
|
|
943
958
|
- lib/ea/version.rb
|
|
944
959
|
- lib/ea/xmi.rb
|
|
945
960
|
- 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
|