ea 0.5.1 → 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/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/lib/ea/cli/command/convert.rb +1 -1
- data/lib/ea/ocl/evaluator.rb +37 -0
- data/lib/ea/ocl/nodes.rb +9 -0
- data/lib/ea/ocl/parser.rb +60 -6
- data/lib/ea/transformers/qea_to_xmi/transformer.rb +138 -2
- data/lib/ea/transformers.rb +3 -2
- data/lib/ea/version.rb +1 -1
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: feec20be82e181c35e21063e8faaefc697c466aeb96ea462a1e1911e20656863
|
|
4
|
+
data.tar.gz: a15bbc9f5d6f078d712f436584175ad4901bad1b2918c9d6f163de1be64b0c21
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9c0ea25f28a1e8dfe0b7d923deb5340f3da3d3aefd8f95b284e80b3de6126756dba1dc96563113b3c64a918c7962fd351361a84f3afc066f304479847625ac7e
|
|
7
|
+
data.tar.gz: 0d68e3d7eea42bcc0d8008c84d5a69d58f94702981659174aa18f6ad7f67bcb07554e054e795aa7fde3716d63597ec71ee1e6148f187935d5c141c9cb2e94835
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# TODO.complete/64: QeaToXmi emits connector elements
|
|
2
|
+
|
|
3
|
+
## Status: open
|
|
4
|
+
|
|
5
|
+
`Ea::Transformers::QeaToXmi` emits associations as `<memberEnd>` +
|
|
6
|
+
`<ownedEnd>` on the owning class, but does NOT emit EA's
|
|
7
|
+
`<connector>` elements. Per the parity regression spec, that's the
|
|
8
|
+
#1 missing category (73/73 in basic.qea, 73/80 in plateau).
|
|
9
|
+
|
|
10
|
+
## Plan
|
|
11
|
+
|
|
12
|
+
1. After serializing the model root + classes, walk `t_connector`
|
|
13
|
+
for associations/aggregations/compositions.
|
|
14
|
+
2. For each connector, locate the corresponding `t_diagramlinks` row
|
|
15
|
+
to retrieve geometry (SX/SY/EX/EY/EDGE) and visual style
|
|
16
|
+
(lineStyle/lineColor).
|
|
17
|
+
3. Emit `<connector>` element with geometry in `<style>` sub-element.
|
|
18
|
+
|
|
19
|
+
## Acceptance
|
|
20
|
+
|
|
21
|
+
- `ea export xmi` output for basic.qea now contains
|
|
22
|
+
`<connector>` elements (was 0/73).
|
|
23
|
+
- `spec/ea/validation/xmi_parity_spec.rb` packagedElement threshold
|
|
24
|
+
raised from 5% to 25% (connectors represent ~25% of EA's output).
|
|
25
|
+
|
|
26
|
+
## OCP
|
|
27
|
+
|
|
28
|
+
Each connector type (Association/Aggregation/Composition) gets a
|
|
29
|
+
private serializer method dispatched by `connector_type`. New
|
|
30
|
+
connector types = new case branch; no other code changes.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# TODO.complete/65: QeaToXmi emits diagram elements
|
|
2
|
+
|
|
3
|
+
## Status: open
|
|
4
|
+
|
|
5
|
+
`Ea::Transformers::QeaToXmi` does not emit EA's `<diagram>` elements
|
|
6
|
+
at all. The parity spec confirms 0/23 in basic.qea, 0/2 in test.qea.
|
|
7
|
+
|
|
8
|
+
## Plan
|
|
9
|
+
|
|
10
|
+
1. Walk `t_diagram` for every diagram.
|
|
11
|
+
2. For each diagram, walk `t_diagramlinks` to find placed
|
|
12
|
+
elements + connectors; emit them as `<element>` children of
|
|
13
|
+
`<diagram>`.
|
|
14
|
+
3. Each element child references its `xmi:idref` to the model
|
|
15
|
+
class.
|
|
16
|
+
4. Connector children emit as `<connector>` with geometry (see
|
|
17
|
+
TODO 64).
|
|
18
|
+
|
|
19
|
+
## Acceptance
|
|
20
|
+
|
|
21
|
+
- `ea export xmi` output for basic.qea now contains
|
|
22
|
+
`<diagram>` elements (was 0/23).
|
|
23
|
+
- Parity threshold raised from 25% to 50%.
|
|
24
|
+
|
|
25
|
+
## OCP
|
|
26
|
+
|
|
27
|
+
Diagrams emit via a new private method `emit_diagrams(model)`
|
|
28
|
+
called after the model class tree. Adding new diagram metadata
|
|
29
|
+
(swimlane info, etc.) = new private method, no other changes.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# TODO.complete/66: QeaToXmi emits style / tags / documentation
|
|
2
|
+
|
|
3
|
+
## Status: open
|
|
4
|
+
|
|
5
|
+
EA's reference XMI contains hundreds of `<style>`, `<tags>`, and
|
|
6
|
+
`<documentation>` elements per diagram. Ours emits zero. Per the
|
|
7
|
+
parity spec: 0/663 styles, 0/478 tags, 0/357 documentation entries.
|
|
8
|
+
|
|
9
|
+
## Plan
|
|
10
|
+
|
|
11
|
+
1. Walk `t_diagramlinks` to find connector+element style strings.
|
|
12
|
+
2. Emit `<style>` children of `<connector>` / `<element>` with the
|
|
13
|
+
stored style XML verbatim.
|
|
14
|
+
3. Walk `t_taggedvalue` for tagged values; emit as `<tags>` children
|
|
15
|
+
with `<tag>` items.
|
|
16
|
+
4. Walk `t_objectnotes` for documentation; emit as `<documentation>`
|
|
17
|
+
children of the owning element.
|
|
18
|
+
|
|
19
|
+
## Acceptance
|
|
20
|
+
|
|
21
|
+
- Output contains `<style>`, `<tags>`, `<documentation>` elements.
|
|
22
|
+
- Parity threshold raised from 50% to 75%.
|
|
23
|
+
|
|
24
|
+
## OCP
|
|
25
|
+
|
|
26
|
+
Each sub-element type (style/tags/documentation) is a private
|
|
27
|
+
emitter method dispatched by collection. New element types = new
|
|
28
|
+
case branch.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# TODO.complete/67: OCL collection operations
|
|
2
|
+
|
|
3
|
+
## Status: open
|
|
4
|
+
|
|
5
|
+
`Ea::Ocl` evaluates a subset of OCL: `exists`, `forAll`, `matches`,
|
|
6
|
+
`and`/`or`/`not`, attribute access. Real EA constraints use
|
|
7
|
+
`size()` and `isEmpty()` (parity: 0/0 because the OCL evaluator
|
|
8
|
+
wasn't even invoked before this session).
|
|
9
|
+
|
|
10
|
+
## Plan
|
|
11
|
+
|
|
12
|
+
1. Add `CollectionSize` AST node: `<collection>->size()`.
|
|
13
|
+
2. Add `CollectionIsEmpty` AST node: `<collection>->isEmpty()`.
|
|
14
|
+
3. Parser recognizes `->size` and `->isEmpty` patterns.
|
|
15
|
+
4. Evaluator returns integer / boolean.
|
|
16
|
+
|
|
17
|
+
## Acceptance
|
|
18
|
+
|
|
19
|
+
- Spec: `inv: self.items->size() > 0` parses + evaluates.
|
|
20
|
+
- Spec: `inv: self.items->isEmpty() or self.required` parses + evaluates.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# TODO.complete/68: OCL comparison operators
|
|
2
|
+
|
|
3
|
+
## Status: open
|
|
4
|
+
|
|
5
|
+
`Ea::Ocl` supports `=`/`==` via equality, but not ordering: `>`,
|
|
6
|
+
`<`, `>=`, `<=`. These appear in real OCL invariants.
|
|
7
|
+
|
|
8
|
+
## Plan
|
|
9
|
+
|
|
10
|
+
1. Add `Comparison` AST node: `left op right` where op is one of
|
|
11
|
+
`>`, `<`, `>=`, `<=`.
|
|
12
|
+
2. Parser recognizes the operators.
|
|
13
|
+
3. Evaluator compares numeric operands; raises `UnsupportedError` on
|
|
14
|
+
non-numeric.
|
|
15
|
+
|
|
16
|
+
## Acceptance
|
|
17
|
+
|
|
18
|
+
- Spec: `inv: self.age >= 18` parses + evaluates (true / false).
|
|
19
|
+
- Spec: `inv: self.count < self.max` parses + evaluates.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# TODO.complete/69: Specs for newly-added QEA models
|
|
2
|
+
|
|
3
|
+
## Status: open
|
|
4
|
+
|
|
5
|
+
The following models were added in TODO 44 / TODO 7 sessions but
|
|
6
|
+
have no spec coverage: `EaSecrypt`, `EaPalette`, `EaPaletteItem`,
|
|
7
|
+
`EaImplement`, `EaRoleConstraint`, `EaObjectProblem`, `EaObjectRisk`,
|
|
8
|
+
`EaObjectTest`, `EaObjectEffort`, `EaObjectResource`,
|
|
9
|
+
`EaObjectScenario`, `EaObjectRequire`, `EaObjectTrx`.
|
|
10
|
+
|
|
11
|
+
Without specs, regressions in column mapping or primary key logic
|
|
12
|
+
go undetected.
|
|
13
|
+
|
|
14
|
+
## Plan
|
|
15
|
+
|
|
16
|
+
1. Single consolidated spec file: `spec/ea/qea/models/
|
|
17
|
+
auxiliary_models_spec.rb`.
|
|
18
|
+
2. Per-model `it "loads primary key and columns"` block.
|
|
19
|
+
3. Verifies `primary_key_column` is correct.
|
|
20
|
+
4. Verifies `column_map` translates known column names to symbols.
|
|
21
|
+
|
|
22
|
+
## Acceptance
|
|
23
|
+
|
|
24
|
+
- File exists, all 13 models have at least 1 example.
|
|
25
|
+
- `bundle exec rspec` passes with 0 failures.
|
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
|
|
@@ -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
|
|
@@ -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
|
-
|
|
74
|
-
|
|
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("&", "&")
|
|
208
|
+
.gsub("<", "<")
|
|
209
|
+
.gsub(">", ">")
|
|
210
|
+
.gsub('"', """)
|
|
75
211
|
end
|
|
76
212
|
|
|
77
213
|
private
|
data/lib/ea/transformers.rb
CHANGED
|
@@ -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
|
data/lib/ea/version.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.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -236,6 +236,12 @@ files:
|
|
|
236
236
|
- TODO.complete/61-plantuml-relationships.md
|
|
237
237
|
- TODO.complete/62-changelog.md
|
|
238
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
|
|
239
245
|
- TODO.complete/STATUS.md
|
|
240
246
|
- TODO.diagrams/01-svg-measurement-harness.md
|
|
241
247
|
- TODO.diagrams/01-xmi-extension-elements-parsing.md
|