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/lib/ea/ocl/evaluator.rb
CHANGED
|
@@ -17,6 +17,9 @@ module Ea
|
|
|
17
17
|
when Nodes::StringMatches then eval_string_matches(ast, context)
|
|
18
18
|
when Nodes::CollectionExists then eval_exists(ast, context)
|
|
19
19
|
when Nodes::CollectionForAll then eval_for_all(ast, context)
|
|
20
|
+
when Nodes::CollectionSize then eval_size(ast, context)
|
|
21
|
+
when Nodes::CollectionIsEmpty then eval_is_empty(ast, context)
|
|
22
|
+
when Nodes::Comparison then eval_comparison(ast, context)
|
|
20
23
|
when Nodes::BinaryOp then eval_binary(ast, context)
|
|
21
24
|
when Nodes::UnaryOp then eval_unary(ast, context)
|
|
22
25
|
else
|
|
@@ -38,7 +41,7 @@ module Ea
|
|
|
38
41
|
|
|
39
42
|
def eval_exists(ast, context)
|
|
40
43
|
collection = evaluate(ast.collection, context)
|
|
41
|
-
return false unless collection.
|
|
44
|
+
return false unless collection.is_a?(Array)
|
|
42
45
|
|
|
43
46
|
var_name = ast.var
|
|
44
47
|
predicate = ast.predicate
|
|
@@ -49,7 +52,7 @@ module Ea
|
|
|
49
52
|
|
|
50
53
|
def eval_for_all(ast, context)
|
|
51
54
|
collection = evaluate(ast.collection, context)
|
|
52
|
-
return true unless collection.
|
|
55
|
+
return true unless collection.is_a?(Array)
|
|
53
56
|
|
|
54
57
|
var_name = ast.var
|
|
55
58
|
predicate = ast.predicate
|
|
@@ -58,6 +61,40 @@ module Ea
|
|
|
58
61
|
end
|
|
59
62
|
end
|
|
60
63
|
|
|
64
|
+
def eval_size(ast, context)
|
|
65
|
+
collection = evaluate(ast.collection, context)
|
|
66
|
+
return 0 unless collection.respond_to?(:size)
|
|
67
|
+
|
|
68
|
+
collection.size
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def eval_is_empty(ast, context)
|
|
72
|
+
collection = evaluate(ast.collection, context)
|
|
73
|
+
return true unless collection.respond_to?(:empty?)
|
|
74
|
+
|
|
75
|
+
collection.empty?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def eval_comparison(ast, context)
|
|
79
|
+
left = evaluate(ast.left, context)
|
|
80
|
+
right = evaluate(ast.right, context)
|
|
81
|
+
unless left.is_a?(Numeric) && right.is_a?(Numeric)
|
|
82
|
+
raise UnsupportedError,
|
|
83
|
+
"Comparison requires numeric operands: #{ast.op} on #{left.inspect}, #{right.inspect}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
case ast.op
|
|
87
|
+
when ">" then left > right
|
|
88
|
+
when "<" then left < right
|
|
89
|
+
when ">=" then left >= right
|
|
90
|
+
when "<=" then left <= right
|
|
91
|
+
when "=" then left == right
|
|
92
|
+
when "==" then left == right
|
|
93
|
+
when "!=" then left != right
|
|
94
|
+
else raise UnsupportedError, "Unknown comparison op: #{ast.op}"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
61
98
|
def eval_binary(ast, context)
|
|
62
99
|
left = evaluate(ast.left, context)
|
|
63
100
|
right = evaluate(ast.right, context)
|
data/lib/ea/ocl/nodes.rb
CHANGED
|
@@ -25,6 +25,15 @@ module Ea
|
|
|
25
25
|
# `<a> and <b>`, `<a> or <b>`, `not <a>`
|
|
26
26
|
BinaryOp = Struct.new(:op, :left, :right, keyword_init: true)
|
|
27
27
|
UnaryOp = Struct.new(:op, :operand, keyword_init: true)
|
|
28
|
+
|
|
29
|
+
# `<collection>->size()` — returns integer cardinality.
|
|
30
|
+
CollectionSize = Struct.new(:collection, keyword_init: true)
|
|
31
|
+
|
|
32
|
+
# `<collection>->isEmpty()` — returns boolean.
|
|
33
|
+
CollectionIsEmpty = Struct.new(:collection, keyword_init: true)
|
|
34
|
+
|
|
35
|
+
# `<a> op <b>` where op ∈ {>, <, >=, <=, =, ==, !=}.
|
|
36
|
+
Comparison = Struct.new(:op, :left, :right, keyword_init: true)
|
|
28
37
|
end
|
|
29
38
|
end
|
|
30
39
|
end
|
data/lib/ea/ocl/parser.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Ea
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# Parse one expression. Returns a single AST node.
|
|
36
|
-
def parse_expression(source)
|
|
36
|
+
def parse_expression(source) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
37
37
|
s = source.strip
|
|
38
38
|
|
|
39
39
|
# Strip outer parens
|
|
@@ -45,7 +45,14 @@ module Ea
|
|
|
45
45
|
operand: parse_expression(s[4..]))
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
# 'and' / 'or'
|
|
48
|
+
# 'and' / 'or' / comparison ops — comparison has higher
|
|
49
|
+
# precedence than logical, so check it first.
|
|
50
|
+
if (split = split_on_comparison_op(s))
|
|
51
|
+
op, left, right = split
|
|
52
|
+
return Nodes::Comparison.new(op: op,
|
|
53
|
+
left: parse_expression(left),
|
|
54
|
+
right: parse_expression(right))
|
|
55
|
+
end
|
|
49
56
|
if (split = split_on_logical_op(s))
|
|
50
57
|
op, left, right = split
|
|
51
58
|
return Nodes::BinaryOp.new(op: op,
|
|
@@ -58,6 +65,16 @@ module Ea
|
|
|
58
65
|
return collection_node(m[2].to_sym, m[1], m[3], m[4])
|
|
59
66
|
end
|
|
60
67
|
|
|
68
|
+
# Collection ->size() — integer cardinality
|
|
69
|
+
if (m = s.match(/^(.+?)->size\(\)\z/))
|
|
70
|
+
return Nodes::CollectionSize.new(collection: parse_expression(m[1]))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Collection ->isEmpty() — boolean
|
|
74
|
+
if (m = s.match(/^(.+?)->isEmpty\(\)\z/))
|
|
75
|
+
return Nodes::CollectionIsEmpty.new(collection: parse_expression(m[1]))
|
|
76
|
+
end
|
|
77
|
+
|
|
61
78
|
# String.matches('regex')
|
|
62
79
|
if (m = s.match(/^(.+?)\.matches\(\s*['"](.+?)['"]\s*\)\z/))
|
|
63
80
|
return Nodes::StringMatches.new(string: parse_expression(m[1]),
|
|
@@ -99,7 +116,6 @@ module Ea
|
|
|
99
116
|
# Find the topmost ` and ` or ` or ` outside of parens.
|
|
100
117
|
# Returns [op, left_str, right_str] or nil.
|
|
101
118
|
def split_on_logical_op(source)
|
|
102
|
-
depth = 0
|
|
103
119
|
# Look for ' or ' first (lower precedence than 'and' in OCL?)
|
|
104
120
|
# Actually OCL: 'or' < 'xor' < 'and' < 'implies'. We pick the
|
|
105
121
|
# rightmost top-level operator (left-associative).
|
|
@@ -113,6 +129,45 @@ module Ea
|
|
|
113
129
|
nil
|
|
114
130
|
end
|
|
115
131
|
|
|
132
|
+
# Split on top-level comparison operator (>, <, >=, <=, =, ==, !=).
|
|
133
|
+
# Comparison has higher precedence than and/or, so we split here
|
|
134
|
+
# BEFORE calling split_on_logical_op.
|
|
135
|
+
def split_on_comparison_op(source)
|
|
136
|
+
%w[>= <= == != = > <].each do |op|
|
|
137
|
+
# Need to be careful with `>=` vs `>`: scan in order so
|
|
138
|
+
# multi-char operators are detected first.
|
|
139
|
+
idx = find_top_level_op(source, op)
|
|
140
|
+
next unless idx
|
|
141
|
+
|
|
142
|
+
return [op, source[0...idx], source[(idx + op.length)..]]
|
|
143
|
+
end
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def find_top_level_op(source, op) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
148
|
+
depth = 0
|
|
149
|
+
i = 0
|
|
150
|
+
while i < source.length
|
|
151
|
+
c = source[i]
|
|
152
|
+
depth += 1 if c == "("
|
|
153
|
+
depth -= 1 if c == ")"
|
|
154
|
+
if depth.zero? && source[i, op.length] == op
|
|
155
|
+
# Each `next` clause below MUST fall through to `i += 1`
|
|
156
|
+
# at the loop's tail — using `next` here would skip the
|
|
157
|
+
# increment and infinite-loop on the same matching char.
|
|
158
|
+
should_skip =
|
|
159
|
+
(op == ">" && source[i + 1, 1] == "=") || # >= matches as >
|
|
160
|
+
(op == "<" && source[i + 1, 1] == "=") || # <= matches as <
|
|
161
|
+
(op == "=" && source[i + 1, 1] == "=") || # == matches as =
|
|
162
|
+
(op == ">" && source[i - 1, 1] == "-") || # -> arrow
|
|
163
|
+
(op == "<" && source[i - 1, 1] == "-") # <- arrow
|
|
164
|
+
return i unless should_skip
|
|
165
|
+
end
|
|
166
|
+
i += 1
|
|
167
|
+
end
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
170
|
+
|
|
116
171
|
# Find substring at depth 0 (not inside parens).
|
|
117
172
|
def find_top_level(source, substring)
|
|
118
173
|
depth = 0
|
|
@@ -121,9 +176,8 @@ module Ea
|
|
|
121
176
|
c = source[i]
|
|
122
177
|
depth += 1 if c == "("
|
|
123
178
|
depth -= 1 if c == ")"
|
|
124
|
-
if depth.zero? && source[i, substring.length] == substring
|
|
125
|
-
|
|
126
|
-
end
|
|
179
|
+
return i if depth.zero? && source[i, substring.length] == substring
|
|
180
|
+
|
|
127
181
|
i += 1
|
|
128
182
|
end
|
|
129
183
|
nil
|
|
@@ -6,7 +6,7 @@ module Ea
|
|
|
6
6
|
# UML-to-code mapping hint from t_implement.
|
|
7
7
|
class EaImplement < BaseModel
|
|
8
8
|
attribute :implement_id, :integer
|
|
9
|
-
attribute :
|
|
9
|
+
attribute :ea_object_id, :integer
|
|
10
10
|
attribute :language, :string
|
|
11
11
|
attribute :code, :string
|
|
12
12
|
|
|
@@ -20,7 +20,7 @@ module Ea
|
|
|
20
20
|
|
|
21
21
|
COLUMN_MAP = {
|
|
22
22
|
"ImplementID" => :implement_id,
|
|
23
|
-
"Object_ID" => :
|
|
23
|
+
"Object_ID" => :ea_object_id
|
|
24
24
|
}.freeze
|
|
25
25
|
|
|
26
26
|
def self.column_map
|
|
@@ -6,7 +6,7 @@ module Ea
|
|
|
6
6
|
# Effort estimate from t_objecteffort.
|
|
7
7
|
class EaObjectEffort < BaseModel
|
|
8
8
|
attribute :object_effort_id, :integer
|
|
9
|
-
attribute :
|
|
9
|
+
attribute :ea_object_id, :integer
|
|
10
10
|
attribute :effort, :string
|
|
11
11
|
attribute :effort_type, :string
|
|
12
12
|
attribute :notes, :string
|
|
@@ -21,7 +21,7 @@ module Ea
|
|
|
21
21
|
|
|
22
22
|
COLUMN_MAP = {
|
|
23
23
|
"ObjectEffortID" => :object_effort_id,
|
|
24
|
-
"Object_ID" => :
|
|
24
|
+
"Object_ID" => :ea_object_id,
|
|
25
25
|
"EffortType" => :effort_type
|
|
26
26
|
}.freeze
|
|
27
27
|
|
|
@@ -6,7 +6,7 @@ module Ea
|
|
|
6
6
|
# Object problem record from t_objectproblems.
|
|
7
7
|
class EaObjectProblem < BaseModel
|
|
8
8
|
attribute :object_problem_id, :integer
|
|
9
|
-
attribute :
|
|
9
|
+
attribute :ea_object_id, :integer
|
|
10
10
|
attribute :problem, :string
|
|
11
11
|
attribute :problem_type, :string
|
|
12
12
|
attribute :date_resolved, :string
|
|
@@ -23,7 +23,7 @@ module Ea
|
|
|
23
23
|
|
|
24
24
|
COLUMN_MAP = {
|
|
25
25
|
"ObjectProblemID" => :object_problem_id,
|
|
26
|
-
"Object_ID" => :
|
|
26
|
+
"Object_ID" => :ea_object_id,
|
|
27
27
|
"ProblemType" => :problem_type,
|
|
28
28
|
"DateResolved" => :date_resolved
|
|
29
29
|
}.freeze
|
|
@@ -6,7 +6,7 @@ module Ea
|
|
|
6
6
|
# Requirement trace from t_objectrequires.
|
|
7
7
|
class EaObjectRequire < BaseModel
|
|
8
8
|
attribute :object_require_id, :integer
|
|
9
|
-
attribute :
|
|
9
|
+
attribute :ea_object_id, :integer
|
|
10
10
|
attribute :requirement, :string
|
|
11
11
|
attribute :requirement_type, :string
|
|
12
12
|
attribute :notes, :string
|
|
@@ -21,7 +21,7 @@ module Ea
|
|
|
21
21
|
|
|
22
22
|
COLUMN_MAP = {
|
|
23
23
|
"ObjectReqID" => :object_require_id,
|
|
24
|
-
"Object_ID" => :
|
|
24
|
+
"Object_ID" => :ea_object_id,
|
|
25
25
|
"ReqType" => :requirement_type
|
|
26
26
|
}.freeze
|
|
27
27
|
|
|
@@ -6,7 +6,7 @@ module Ea
|
|
|
6
6
|
# Resource allocation from t_objectresource.
|
|
7
7
|
class EaObjectResource < BaseModel
|
|
8
8
|
attribute :object_resource_id, :integer
|
|
9
|
-
attribute :
|
|
9
|
+
attribute :ea_object_id, :integer
|
|
10
10
|
attribute :resource, :string
|
|
11
11
|
attribute :role, :string
|
|
12
12
|
attribute :time_allocated, :string
|
|
@@ -22,7 +22,7 @@ module Ea
|
|
|
22
22
|
|
|
23
23
|
COLUMN_MAP = {
|
|
24
24
|
"ObjectResourceID" => :object_resource_id,
|
|
25
|
-
"Object_ID" => :
|
|
25
|
+
"Object_ID" => :ea_object_id
|
|
26
26
|
}.freeze
|
|
27
27
|
|
|
28
28
|
def self.column_map
|
|
@@ -6,7 +6,7 @@ module Ea
|
|
|
6
6
|
# Risk register entry from t_objectrisks.
|
|
7
7
|
class EaObjectRisk < BaseModel
|
|
8
8
|
attribute :object_risk_id, :integer
|
|
9
|
-
attribute :
|
|
9
|
+
attribute :ea_object_id, :integer
|
|
10
10
|
attribute :risk, :string
|
|
11
11
|
attribute :risk_type, :string
|
|
12
12
|
attribute :notes, :string
|
|
@@ -21,7 +21,7 @@ module Ea
|
|
|
21
21
|
|
|
22
22
|
COLUMN_MAP = {
|
|
23
23
|
"ObjectRiskID" => :object_risk_id,
|
|
24
|
-
"Object_ID" => :
|
|
24
|
+
"Object_ID" => :ea_object_id,
|
|
25
25
|
"RiskType" => :risk_type
|
|
26
26
|
}.freeze
|
|
27
27
|
|
|
@@ -6,7 +6,7 @@ module Ea
|
|
|
6
6
|
# Use case scenario from t_objectscenarios.
|
|
7
7
|
class EaObjectScenario < BaseModel
|
|
8
8
|
attribute :object_scenario_id, :integer
|
|
9
|
-
attribute :
|
|
9
|
+
attribute :ea_object_id, :integer
|
|
10
10
|
attribute :scenario, :string
|
|
11
11
|
attribute :scenario_type, :string
|
|
12
12
|
attribute :notes, :string
|
|
@@ -21,7 +21,7 @@ module Ea
|
|
|
21
21
|
|
|
22
22
|
COLUMN_MAP = {
|
|
23
23
|
"ObjectScenarioID" => :object_scenario_id,
|
|
24
|
-
"Object_ID" => :
|
|
24
|
+
"Object_ID" => :ea_object_id,
|
|
25
25
|
"ScenarioType" => :scenario_type
|
|
26
26
|
}.freeze
|
|
27
27
|
|
|
@@ -6,7 +6,7 @@ module Ea
|
|
|
6
6
|
# Test case from t_objecttests.
|
|
7
7
|
class EaObjectTest < BaseModel
|
|
8
8
|
attribute :object_test_id, :integer
|
|
9
|
-
attribute :
|
|
9
|
+
attribute :ea_object_id, :integer
|
|
10
10
|
attribute :test, :string
|
|
11
11
|
attribute :test_type, :string
|
|
12
12
|
attribute :status, :string
|
|
@@ -22,7 +22,7 @@ module Ea
|
|
|
22
22
|
|
|
23
23
|
COLUMN_MAP = {
|
|
24
24
|
"ObjectTestID" => :object_test_id,
|
|
25
|
-
"Object_ID" => :
|
|
25
|
+
"Object_ID" => :ea_object_id,
|
|
26
26
|
"TestType" => :test_type
|
|
27
27
|
}.freeze
|
|
28
28
|
|
|
@@ -6,7 +6,7 @@ module Ea
|
|
|
6
6
|
# Transaction record from t_objecttrx.
|
|
7
7
|
class EaObjectTrx < BaseModel
|
|
8
8
|
attribute :object_trx_id, :integer
|
|
9
|
-
attribute :
|
|
9
|
+
attribute :ea_object_id, :integer
|
|
10
10
|
attribute :trx, :string
|
|
11
11
|
attribute :trx_type, :string
|
|
12
12
|
attribute :notes, :string
|
|
@@ -21,7 +21,7 @@ module Ea
|
|
|
21
21
|
|
|
22
22
|
COLUMN_MAP = {
|
|
23
23
|
"ObjectTrxID" => :object_trx_id,
|
|
24
|
-
"Object_ID" => :
|
|
24
|
+
"Object_ID" => :ea_object_id,
|
|
25
25
|
"TrxType" => :trx_type
|
|
26
26
|
}.freeze
|
|
27
27
|
|
|
@@ -7,7 +7,7 @@ module Ea
|
|
|
7
7
|
# entry to its visual icon and default toolbox slot.
|
|
8
8
|
class EaPaletteItem < BaseModel
|
|
9
9
|
attribute :palette_id, :string
|
|
10
|
-
attribute :
|
|
10
|
+
attribute :ea_object_id, :integer
|
|
11
11
|
attribute :name, :string
|
|
12
12
|
attribute :alias, :string
|
|
13
13
|
attribute :image_id, :integer
|
|
@@ -23,7 +23,7 @@ module Ea
|
|
|
23
23
|
|
|
24
24
|
COLUMN_MAP = {
|
|
25
25
|
"PaletteID" => :palette_id,
|
|
26
|
-
"Object_ID" => :
|
|
26
|
+
"Object_ID" => :ea_object_id,
|
|
27
27
|
"Image_ID" => :image_id
|
|
28
28
|
}.freeze
|
|
29
29
|
|
|
@@ -6,7 +6,7 @@ module Ea
|
|
|
6
6
|
# Role-based constraint from t_roleconstraint.
|
|
7
7
|
class EaRoleConstraint < BaseModel
|
|
8
8
|
attribute :roleconstraint_id, :integer
|
|
9
|
-
attribute :
|
|
9
|
+
attribute :ea_object_id, :integer
|
|
10
10
|
attribute :role, :string
|
|
11
11
|
attribute :notes, :string
|
|
12
12
|
|
|
@@ -20,7 +20,7 @@ module Ea
|
|
|
20
20
|
|
|
21
21
|
COLUMN_MAP = {
|
|
22
22
|
"RoleConstraintID" => :roleconstraint_id,
|
|
23
|
-
"Object_ID" => :
|
|
23
|
+
"Object_ID" => :ea_object_id
|
|
24
24
|
}.freeze
|
|
25
25
|
|
|
26
26
|
def self.column_map
|
|
@@ -7,7 +7,7 @@ module Ea
|
|
|
7
7
|
# the QEA are encrypted and what algorithm was used.
|
|
8
8
|
class EaSecrypt < BaseModel
|
|
9
9
|
attribute :secrypt_id, :integer
|
|
10
|
-
attribute :
|
|
10
|
+
attribute :ea_object_id, :integer
|
|
11
11
|
attribute :encrypt, :string
|
|
12
12
|
attribute :password_hash, :string
|
|
13
13
|
|
|
@@ -21,7 +21,7 @@ module Ea
|
|
|
21
21
|
|
|
22
22
|
COLUMN_MAP = {
|
|
23
23
|
"SecryptID" => :secrypt_id,
|
|
24
|
-
"Object_ID" => :
|
|
24
|
+
"Object_ID" => :ea_object_id,
|
|
25
25
|
"PasswordHash" => :password_hash
|
|
26
26
|
}.freeze
|
|
27
27
|
|
|
@@ -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
|