ea 0.2.5 → 0.3.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/TODO.refactor/01-ea-model-domain-layer.md +64 -0
- data/TODO.refactor/02-ea-model-qea-source-adapter.md +58 -0
- data/TODO.refactor/03-ea-model-xmi-source-adapter.md +33 -0
- data/TODO.refactor/04-ea-model-annotations-tagged-values.md +34 -0
- data/TODO.refactor/05-ea-model-diagrams-coordinates.md +37 -0
- data/TODO.refactor/06-spa-projection-layer.md +45 -0
- data/TODO.refactor/07-spa-sharded-output-strategy.md +28 -0
- data/TODO.refactor/08-spa-build-time-search-index.md +24 -0
- data/TODO.refactor/09-spa-frontend-data-contract.md +25 -0
- data/TODO.refactor/10-spa-cli-command-rework.md +34 -0
- data/TODO.refactor/11-spa-perf-benchmark.md +23 -0
- data/TODO.refactor/12-lutaml-uml-static-site-deprecation.md +28 -0
- data/lib/ea/cli/app.rb +13 -7
- data/lib/ea/cli/command/spa.rb +33 -48
- data/lib/ea/cli/command/svg.rb +68 -0
- data/lib/ea/cli/command.rb +1 -0
- data/lib/ea/model/annotation.rb +29 -0
- data/lib/ea/model/association.rb +40 -0
- data/lib/ea/model/base.rb +27 -0
- data/lib/ea/model/bounds.rb +22 -0
- data/lib/ea/model/classifier.rb +41 -0
- data/lib/ea/model/data_type.rb +9 -0
- data/lib/ea/model/dependency.rb +19 -0
- data/lib/ea/model/diagram.rb +28 -0
- data/lib/ea/model/diagram_connector.rb +30 -0
- data/lib/ea/model/diagram_element.rb +23 -0
- data/lib/ea/model/document.rb +107 -0
- data/lib/ea/model/enumeration.rb +15 -0
- data/lib/ea/model/enumeration_literal.rb +16 -0
- data/lib/ea/model/generalization.rb +19 -0
- data/lib/ea/model/interface.rb +9 -0
- data/lib/ea/model/klass.rb +16 -0
- data/lib/ea/model/metadata.rb +31 -0
- data/lib/ea/model/operation.rb +33 -0
- data/lib/ea/model/package.rb +36 -0
- data/lib/ea/model/parameter.rb +24 -0
- data/lib/ea/model/point.rb +16 -0
- data/lib/ea/model/polymorphic_maps.rb +30 -0
- data/lib/ea/model/primitive_type.rb +9 -0
- data/lib/ea/model/property.rb +49 -0
- data/lib/ea/model/realization.rb +19 -0
- data/lib/ea/model/relationship.rb +21 -0
- data/lib/ea/model/stereotype.rb +24 -0
- data/lib/ea/model/tagged_value.rb +20 -0
- data/lib/ea/model/waypoint.rb +19 -0
- data/lib/ea/model.rb +48 -0
- data/lib/ea/sources/qea/adapter.rb +63 -0
- data/lib/ea/sources/qea/annotation_builder.rb +36 -0
- data/lib/ea/sources/qea/classifier_builder.rb +119 -0
- data/lib/ea/sources/qea/connector_relationship_map.rb +42 -0
- data/lib/ea/sources/qea/diagram_builder.rb +214 -0
- data/lib/ea/sources/qea/diagram_style_parser.rb +43 -0
- data/lib/ea/sources/qea/id_normalizer.rb +24 -0
- data/lib/ea/sources/qea/metadata_builder.rb +42 -0
- data/lib/ea/sources/qea/object_classifier_map.rb +36 -0
- data/lib/ea/sources/qea/operation_builder.rb +79 -0
- data/lib/ea/sources/qea/package_builder.rb +44 -0
- data/lib/ea/sources/qea/property_builder.rb +86 -0
- data/lib/ea/sources/qea/relationship_builder.rb +113 -0
- data/lib/ea/sources/qea/stereotype_builder.rb +44 -0
- data/lib/ea/sources/qea/tagged_value_builder.rb +39 -0
- data/lib/ea/sources/qea.rb +31 -0
- data/lib/ea/sources/xmi/adapter.rb +62 -0
- data/lib/ea/sources/xmi/annotation_builder.rb +49 -0
- data/lib/ea/sources/xmi/classifier_builder.rb +89 -0
- data/lib/ea/sources/xmi/diagram_builder.rb +82 -0
- data/lib/ea/sources/xmi/id_normalizer.rb +27 -0
- data/lib/ea/sources/xmi/metadata_builder.rb +32 -0
- data/lib/ea/sources/xmi/operation_builder.rb +59 -0
- data/lib/ea/sources/xmi/package_builder.rb +73 -0
- data/lib/ea/sources/xmi/property_builder.rb +77 -0
- data/lib/ea/sources/xmi/relationship_builder.rb +142 -0
- data/lib/ea/sources/xmi/relationship_type_map.rb +34 -0
- data/lib/ea/sources/xmi/type_classifier_map.rb +33 -0
- data/lib/ea/sources/xmi.rb +29 -0
- data/lib/ea/sources.rb +15 -0
- data/lib/ea/spa/configuration.rb +110 -0
- data/lib/ea/spa/generator.rb +45 -0
- data/lib/ea/spa/lazy_ref.rb +18 -0
- data/lib/ea/spa/output/sharded_multi_file_strategy.rb +71 -0
- data/lib/ea/spa/output/single_file_strategy.rb +56 -0
- data/lib/ea/spa/output/strategy.rb +36 -0
- data/lib/ea/spa/output.rb +11 -0
- data/lib/ea/spa/package_tree.rb +15 -0
- data/lib/ea/spa/package_tree_node.rb +26 -0
- data/lib/ea/spa/projector.rb +199 -0
- data/lib/ea/spa/search_entry.rb +28 -0
- data/lib/ea/spa/search_index.rb +18 -0
- data/lib/ea/spa/shard.rb +19 -0
- data/lib/ea/spa/skeleton.rb +22 -0
- data/lib/ea/spa/skeleton_entry.rb +27 -0
- data/lib/ea/spa.rb +31 -0
- data/lib/ea/svg/bounds_calculator.rb +74 -0
- data/lib/ea/svg/connector_path.rb +138 -0
- data/lib/ea/svg/element_box.rb +256 -0
- data/lib/ea/svg/renderer.rb +77 -0
- data/lib/ea/svg/style_resolver.rb +72 -0
- data/lib/ea/svg.rb +21 -0
- data/lib/ea/version.rb +1 -1
- data/lib/ea.rb +4 -0
- metadata +97 -2
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Qea
|
|
6
|
+
# Translates EA t_diagram + t_diagramlinks + t_diagramobjects
|
|
7
|
+
# into Ea::Model::Diagram with placed DiagramElements and
|
|
8
|
+
# DiagramConnectors (including pixel coordinates and parsed
|
|
9
|
+
# styles).
|
|
10
|
+
class DiagramBuilder
|
|
11
|
+
attr_reader :database
|
|
12
|
+
|
|
13
|
+
def initialize(database)
|
|
14
|
+
@database = database
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build_all
|
|
18
|
+
diagrams = database.collections[:diagrams] || []
|
|
19
|
+
diagrams.map { |row| build_one(row) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build_one(diagram_row)
|
|
23
|
+
Ea::Model::Diagram.new(
|
|
24
|
+
id: IdNormalizer.from_guid(diagram_row.ea_guid),
|
|
25
|
+
name: diagram_row.name,
|
|
26
|
+
package_id: package_id_for(diagram_row),
|
|
27
|
+
diagram_type: diagram_row.diagram_type,
|
|
28
|
+
bounds: bounds_for(diagram_row),
|
|
29
|
+
elements: build_elements(diagram_row),
|
|
30
|
+
connectors: build_connectors(diagram_row),
|
|
31
|
+
annotations: AnnotationBuilder.from_note(diagram_row.notes,
|
|
32
|
+
diagram_row.ea_guid,
|
|
33
|
+
kind: "documentation")
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def build_elements(diagram_row)
|
|
40
|
+
objs = database.diagram_objects_for(diagram_row.diagram_id) || []
|
|
41
|
+
objs.map { |obj_row| build_element(obj_row, diagram_row) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_element(obj_row, diagram_row)
|
|
45
|
+
Ea::Model::DiagramElement.new(
|
|
46
|
+
id: IdNormalizer.synthetic("de", diagram_row.diagram_id,
|
|
47
|
+
obj_row.instance_id),
|
|
48
|
+
diagram_id: IdNormalizer.from_guid(diagram_row.ea_guid),
|
|
49
|
+
model_element_ref: ref_for_object(obj_row),
|
|
50
|
+
bounds: bounds_from_rect(obj_row),
|
|
51
|
+
style: DiagramStyleParser.parse(obj_row.objectstyle)
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_connectors(diagram_row)
|
|
56
|
+
links = database.diagram_links_for(diagram_row.diagram_id) || []
|
|
57
|
+
links.map { |link_row| build_connector(link_row, diagram_row) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_connector(link_row, diagram_row)
|
|
61
|
+
Ea::Model::DiagramConnector.new(
|
|
62
|
+
id: IdNormalizer.synthetic("dc", diagram_row.diagram_id,
|
|
63
|
+
link_row.instance_id),
|
|
64
|
+
diagram_id: IdNormalizer.from_guid(diagram_row.ea_guid),
|
|
65
|
+
relationship_ref: ref_for_connector(link_row),
|
|
66
|
+
waypoints: waypoints_for_link(link_row, diagram_row),
|
|
67
|
+
style: DiagramStyleParser.parse(link_row.style)
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# EA's t_diagramlinks.Geometry field stores connector routing
|
|
72
|
+
# in a packed format. The leading "X1,Y1,X2,Y2" pair (when
|
|
73
|
+
# present) is in a coordinate frame that does NOT match the
|
|
74
|
+
# element rect frame (elements use RectTop < RectBottom with
|
|
75
|
+
# negative y; geometry uses positive y). Rather than trust
|
|
76
|
+
# the stored absolute coords, we compute the polyline from
|
|
77
|
+
# the actual placed-element bounds + the SX/SY/EX/EY delta
|
|
78
|
+
# fields, which describe the bend routing relative to the
|
|
79
|
+
# element edges.
|
|
80
|
+
#
|
|
81
|
+
# If we can't resolve the source or target element placement
|
|
82
|
+
# for this diagram, we emit no waypoints (the connector will
|
|
83
|
+
# be invisible) — better than drawing garbage lines.
|
|
84
|
+
def waypoints_for_link(link_row, diagram_row)
|
|
85
|
+
connector = database.find_connector(link_row.connectorid)
|
|
86
|
+
return [] unless connector
|
|
87
|
+
|
|
88
|
+
source_placement = diagram_object_placement(diagram_row.diagram_id,
|
|
89
|
+
connector.start_object_id)
|
|
90
|
+
target_placement = diagram_object_placement(diagram_row.diagram_id,
|
|
91
|
+
connector.end_object_id)
|
|
92
|
+
return [] unless source_placement && target_placement
|
|
93
|
+
|
|
94
|
+
geom = parse_geometry_fields(link_row.geometry)
|
|
95
|
+
edge_out = geom[:edge] || 0
|
|
96
|
+
|
|
97
|
+
source_point = element_edge_point(source_placement, :source, edge_out)
|
|
98
|
+
target_point = element_edge_point(target_placement, :target, edge_out)
|
|
99
|
+
points = [source_point]
|
|
100
|
+
# SX/SY: delta from source point to first bend.
|
|
101
|
+
if geom[:sx] && geom[:sy] && (geom[:sx].nonzero? || geom[:sy].nonzero?)
|
|
102
|
+
points << [source_point[0] + geom[:sx], source_point[1] + geom[:sy]]
|
|
103
|
+
end
|
|
104
|
+
# EX/EY: delta from second bend to target point. Reverse
|
|
105
|
+
# to get from target back to the bend.
|
|
106
|
+
if geom[:ex] && geom[:ey] && (geom[:ex].nonzero? || geom[:ey].nonzero?)
|
|
107
|
+
points << [target_point[0] - geom[:ex], target_point[1] - geom[:ey]]
|
|
108
|
+
end
|
|
109
|
+
points << target_point
|
|
110
|
+
|
|
111
|
+
points.map do |x, y|
|
|
112
|
+
Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y))
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Parse the SX, SY, EX, EY, EDGE key=value pairs out of the
|
|
117
|
+
# geometry string. Each is `KEY=<int>;`. We deliberately do
|
|
118
|
+
# NOT extract the leading X1,Y1,X2,Y2 numbers because those
|
|
119
|
+
# are in a different coordinate frame (see note above).
|
|
120
|
+
def parse_geometry_fields(geometry)
|
|
121
|
+
return {} if geometry.nil? || geometry.empty?
|
|
122
|
+
|
|
123
|
+
s = geometry.to_s
|
|
124
|
+
{
|
|
125
|
+
sx: pick_int(s, /SX=(-?\d+)/),
|
|
126
|
+
sy: pick_int(s, /SY=(-?\d+)/),
|
|
127
|
+
ex: pick_int(s, /EX=(-?\d+)/),
|
|
128
|
+
ey: pick_int(s, /EY=(-?\d+)/),
|
|
129
|
+
edge: pick_int(s, /EDGE=(-?\d+)/)
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def pick_int(str, pattern)
|
|
134
|
+
match = str.match(pattern)
|
|
135
|
+
return nil unless match
|
|
136
|
+
|
|
137
|
+
Integer(match[1])
|
|
138
|
+
rescue ArgumentError
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Find where a given EA object is placed on this diagram.
|
|
143
|
+
def diagram_object_placement(diagram_id, ea_object_id)
|
|
144
|
+
objects = database.diagram_objects_for(diagram_id) || []
|
|
145
|
+
objects.find { |o| o.ea_object_id == ea_object_id }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Compute the connection point on an element's edge for the
|
|
149
|
+
# given side. EA's `EDGE` field on t_diagramlinks tells us
|
|
150
|
+
# which edge the connector attaches to:
|
|
151
|
+
# 1 = top (source), 2 = right, 3 = bottom, 4 = left,
|
|
152
|
+
# plus 5/6/7/8 for diagonals (treated as the cardinal here).
|
|
153
|
+
# We use the center of the chosen edge as the connection point.
|
|
154
|
+
def element_edge_point(placement, end_kind, edge_code)
|
|
155
|
+
b = bounds_from_rect(placement)
|
|
156
|
+
case effective_edge(edge_code, end_kind)
|
|
157
|
+
when :top then [b.x + b.width / 2, b.y]
|
|
158
|
+
when :right then [b.x + b.width, b.y + b.height / 2]
|
|
159
|
+
when :bottom then [b.x + b.width / 2, b.y + b.height]
|
|
160
|
+
when :left then [b.x, b.y + b.height / 2]
|
|
161
|
+
else [b.x + b.width / 2, b.y + b.height / 2]
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def effective_edge(edge_code, end_kind)
|
|
166
|
+
mapping = {
|
|
167
|
+
1 => :top, 2 => :right, 3 => :bottom, 4 => :left,
|
|
168
|
+
5 => :top, 6 => :right, 7 => :bottom, 8 => :left
|
|
169
|
+
}
|
|
170
|
+
mapping[edge_code.to_i] || :center
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def package_id_for(diagram_row)
|
|
174
|
+
pkg = database.find_package(diagram_row.package_id)
|
|
175
|
+
return nil unless pkg
|
|
176
|
+
|
|
177
|
+
IdNormalizer.from_guid(pkg.ea_guid)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def bounds_for(diagram_row)
|
|
181
|
+
Ea::Model::Bounds.new(
|
|
182
|
+
x: 0,
|
|
183
|
+
y: 0,
|
|
184
|
+
width: diagram_row.cx || 0,
|
|
185
|
+
height: diagram_row.cy || 0
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def bounds_from_rect(obj_row)
|
|
190
|
+
Ea::Model::Bounds.new(
|
|
191
|
+
x: obj_row.rectleft || 0,
|
|
192
|
+
y: obj_row.recttop || 0,
|
|
193
|
+
width: (obj_row.rectright || 0) - (obj_row.rectleft || 0),
|
|
194
|
+
height: (obj_row.rectbottom || 0) - (obj_row.recttop || 0)
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def ref_for_object(obj_row)
|
|
199
|
+
obj = database.find_object(obj_row.ea_object_id)
|
|
200
|
+
return nil unless obj
|
|
201
|
+
|
|
202
|
+
IdNormalizer.from_guid(obj.ea_guid)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def ref_for_connector(link_row)
|
|
206
|
+
conn = database.find_connector(link_row.connectorid)
|
|
207
|
+
return nil unless conn
|
|
208
|
+
|
|
209
|
+
IdNormalizer.from_guid(conn.ea_guid)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Qea
|
|
6
|
+
# Parses EA's packed `objectstyle` / `style` / `styleex`
|
|
7
|
+
# strings into a Hash with typed keys. EA packs visual style
|
|
8
|
+
# as `DUID=...;BCol=...;FCol=...;` etc. — semicolon-separated
|
|
9
|
+
# key=value pairs with non-standard escaping.
|
|
10
|
+
module DiagramStyleParser
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def parse(style_string)
|
|
14
|
+
return {} if style_string.nil? || style_string.empty?
|
|
15
|
+
|
|
16
|
+
style_string.to_s
|
|
17
|
+
.split(";")
|
|
18
|
+
.filter_map { |pair| parse_pair(pair) }
|
|
19
|
+
.to_h
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def parse_pair(pair)
|
|
23
|
+
return nil unless pair.include?("=")
|
|
24
|
+
|
|
25
|
+
key, value = pair.split("=", 2)
|
|
26
|
+
return nil if key.nil? || key.empty?
|
|
27
|
+
|
|
28
|
+
[normalize_key(key), normalize_value(value)]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def normalize_key(key)
|
|
32
|
+
key.strip.downcase.to_sym
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def normalize_value(value)
|
|
36
|
+
return "" if value.nil?
|
|
37
|
+
|
|
38
|
+
value.strip
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Qea
|
|
6
|
+
# Normalizes EA's identifier formats into stable strings used
|
|
7
|
+
# as Ea::Model element ids. EA represents identity as
|
|
8
|
+
# `{GUID}` strings. We strip the braces to give the bare GUID.
|
|
9
|
+
module IdNormalizer
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def from_guid(ea_guid)
|
|
13
|
+
return nil if ea_guid.nil? || ea_guid.empty?
|
|
14
|
+
|
|
15
|
+
ea_guid.to_s.gsub(/[{}]/, "")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def synthetic(prefix, *parts)
|
|
19
|
+
"#{prefix}:#{parts.join(":")}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Qea
|
|
6
|
+
# Builds an Ea::Model::Metadata from the QEA database. Pulls
|
|
7
|
+
# project-level info (model name, version, tool, source path)
|
|
8
|
+
# from the root package and the QEA file path.
|
|
9
|
+
class MetadataBuilder
|
|
10
|
+
attr_reader :database, :qea_path
|
|
11
|
+
|
|
12
|
+
def initialize(database, qea_path)
|
|
13
|
+
@database = database
|
|
14
|
+
@qea_path = qea_path
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build
|
|
18
|
+
root_pkg = root_package
|
|
19
|
+
Ea::Model::Metadata.new(
|
|
20
|
+
id: "metadata",
|
|
21
|
+
title: root_pkg&.name,
|
|
22
|
+
version: root_pkg&.version,
|
|
23
|
+
created_date: root_pkg&.createddate,
|
|
24
|
+
modified_date: root_pkg&.modifieddate,
|
|
25
|
+
author: root_pkg&.pkgowner,
|
|
26
|
+
source_format: "qea",
|
|
27
|
+
source_tool: "Sparx Enterprise Architect",
|
|
28
|
+
source_path: qea_path
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def root_package
|
|
35
|
+
@root_package ||= database.collections[:packages]&.find do |pkg|
|
|
36
|
+
pkg.parent_id.nil? || pkg.parent_id.zero?
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Qea
|
|
6
|
+
# Maps EA's `t_object.Object_Type` discriminator to the
|
|
7
|
+
# concrete Ea::Model::Classifier subclass that should be
|
|
8
|
+
# produced. New classifier kinds = one entry here, nothing
|
|
9
|
+
# else changes (OCP).
|
|
10
|
+
module ObjectClassifierMap
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# EA Object_Type → Model class. Unknown types default to
|
|
14
|
+
# Klass — the most generic classifier.
|
|
15
|
+
LOOKUP = {
|
|
16
|
+
"Class" => "Ea::Model::Klass",
|
|
17
|
+
"DataType" => "Ea::Model::DataType",
|
|
18
|
+
"Enumeration" => "Ea::Model::Enumeration",
|
|
19
|
+
"Interface" => "Ea::Model::Interface",
|
|
20
|
+
"PrimitiveType" => "Ea::Model::PrimitiveType",
|
|
21
|
+
"Primitive" => "Ea::Model::PrimitiveType"
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
def class_for(object_type)
|
|
25
|
+
name = LOOKUP[object_type] || "Ea::Model::Klass"
|
|
26
|
+
Object.const_get(name)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def model_kind_for(object_type)
|
|
30
|
+
cls = class_for(object_type)
|
|
31
|
+
cls.new.model_kind
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Qea
|
|
6
|
+
# Translates EA t_operation + t_operationparams rows into
|
|
7
|
+
# Ea::Model::Operation instances with owned Parameters.
|
|
8
|
+
class OperationBuilder
|
|
9
|
+
attr_reader :database
|
|
10
|
+
|
|
11
|
+
def initialize(database)
|
|
12
|
+
@database = database
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build_all_for(owner_object)
|
|
16
|
+
ops = database.operations_for_object(owner_object.ea_object_id) || []
|
|
17
|
+
ops.map { |row| build_one(row, owner_object) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def build_one(row, owner_object)
|
|
21
|
+
Ea::Model::Operation.new(
|
|
22
|
+
id: IdNormalizer.from_guid(row.ea_guid),
|
|
23
|
+
name: row.name,
|
|
24
|
+
owner_id: IdNormalizer.from_guid(owner_object.ea_guid),
|
|
25
|
+
qualified_name: "#{owner_object.name}::#{row.name}",
|
|
26
|
+
return_type_name: row.type,
|
|
27
|
+
visibility: visibility_from_scope(row.scope),
|
|
28
|
+
is_static: boolean(row.isstatic),
|
|
29
|
+
is_abstract: boolean(row.abstract),
|
|
30
|
+
parameters: build_parameters(row),
|
|
31
|
+
annotations: AnnotationBuilder.from_note(row.notes, row.ea_guid,
|
|
32
|
+
kind: "documentation")
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def build_parameters(operation_row)
|
|
39
|
+
params = database.operation_params_for(operation_row.operationid) || []
|
|
40
|
+
params.map.with_index do |p, idx|
|
|
41
|
+
Ea::Model::Parameter.new(
|
|
42
|
+
id: IdNormalizer.from_guid(p.ea_guid),
|
|
43
|
+
name: p.name,
|
|
44
|
+
ordinal: p.pos || idx,
|
|
45
|
+
direction: direction_from_kind(p.kind),
|
|
46
|
+
type_name: p.type,
|
|
47
|
+
default_value: p.default
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def direction_from_kind(kind)
|
|
53
|
+
case kind.to_s
|
|
54
|
+
when "1", "out" then "out"
|
|
55
|
+
when "2", "inout" then "inout"
|
|
56
|
+
when "3", "return" then "return"
|
|
57
|
+
else "in"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def visibility_from_scope(scope)
|
|
62
|
+
case scope.to_s
|
|
63
|
+
when "Public" then "public"
|
|
64
|
+
when "Protected" then "protected"
|
|
65
|
+
when "Private" then "private"
|
|
66
|
+
else "public"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def boolean(value)
|
|
71
|
+
case value.to_s
|
|
72
|
+
when "1", "true", "TRUE" then true
|
|
73
|
+
else false
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Qea
|
|
6
|
+
# Translates EA t_package rows into Ea::Model::Package
|
|
7
|
+
# instances. References to parent and child packages are by
|
|
8
|
+
# model id (normalized GUID), not by EA's integer Package_ID.
|
|
9
|
+
class PackageBuilder
|
|
10
|
+
attr_reader :database
|
|
11
|
+
|
|
12
|
+
def initialize(database)
|
|
13
|
+
@database = database
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build_all
|
|
17
|
+
packages = database.collections[:packages] || []
|
|
18
|
+
packages.map { |pkg| build_one(pkg) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build_one(pkg)
|
|
22
|
+
Ea::Model::Package.new(
|
|
23
|
+
id: IdNormalizer.from_guid(pkg.ea_guid),
|
|
24
|
+
name: pkg.name,
|
|
25
|
+
parent_id: parent_id_for(pkg),
|
|
26
|
+
qualified_name: pkg.xmlpath,
|
|
27
|
+
annotations: AnnotationBuilder.from_note(pkg.notes, pkg.ea_guid)
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def parent_id_for(pkg)
|
|
34
|
+
return nil if pkg.parent_id.nil? || pkg.parent_id.zero?
|
|
35
|
+
|
|
36
|
+
parent = database.find_package(pkg.parent_id)
|
|
37
|
+
return nil unless parent
|
|
38
|
+
|
|
39
|
+
IdNormalizer.from_guid(parent.ea_guid)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Qea
|
|
6
|
+
# Translates EA t_attribute rows into Ea::Model::Property
|
|
7
|
+
# instances. Multiplicity is parsed from EA's `LowerBound` /
|
|
8
|
+
# `UpperBound` columns (with `*` represented as -1 in the
|
|
9
|
+
# model).
|
|
10
|
+
class PropertyBuilder
|
|
11
|
+
attr_reader :database
|
|
12
|
+
|
|
13
|
+
def initialize(database)
|
|
14
|
+
@database = database
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build_all_for(owner_object)
|
|
18
|
+
attrs = database.attributes_for_object(owner_object.ea_object_id) || []
|
|
19
|
+
attrs.map { |row| build_one(row, owner_object) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build_one(row, owner_object)
|
|
23
|
+
Ea::Model::Property.new(
|
|
24
|
+
id: IdNormalizer.from_guid(row.ea_guid),
|
|
25
|
+
name: row.name,
|
|
26
|
+
owner_id: IdNormalizer.from_guid(owner_object.ea_guid),
|
|
27
|
+
type_name: row.type,
|
|
28
|
+
qualified_name: "#{owner_object.name}::#{row.name}",
|
|
29
|
+
multiplicity_lower: parse_lower(row),
|
|
30
|
+
multiplicity_upper: parse_upper(row),
|
|
31
|
+
default_value: row.default,
|
|
32
|
+
is_ordered: boolean(row.isordered),
|
|
33
|
+
is_unique: !boolean(row.allowduplicates),
|
|
34
|
+
visibility: visibility_from_scope(row.scope),
|
|
35
|
+
aggregation: "none",
|
|
36
|
+
stereotype_refs: stereotype_refs(row),
|
|
37
|
+
tagged_values: TaggedValueBuilder.new(database).for_attribute(row),
|
|
38
|
+
annotations: AnnotationBuilder.from_note(row.notes, row.ea_guid,
|
|
39
|
+
kind: "documentation")
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def parse_lower(row)
|
|
46
|
+
Integer(row.lowerbound || 1)
|
|
47
|
+
rescue StandardError
|
|
48
|
+
1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse_upper(row)
|
|
52
|
+
return -1 if row.upperbound.to_s == "*"
|
|
53
|
+
|
|
54
|
+
begin
|
|
55
|
+
Integer(row.upperbound || 1)
|
|
56
|
+
rescue StandardError
|
|
57
|
+
1
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def boolean(value)
|
|
62
|
+
case value.to_s
|
|
63
|
+
when "1", "true", "TRUE" then true
|
|
64
|
+
else false
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def visibility_from_scope(scope)
|
|
69
|
+
case scope.to_s
|
|
70
|
+
when "Public" then "public"
|
|
71
|
+
when "Protected" then "protected"
|
|
72
|
+
when "Private" then "private"
|
|
73
|
+
when "Package" then "package"
|
|
74
|
+
else "public"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def stereotype_refs(row)
|
|
79
|
+
refs = []
|
|
80
|
+
refs << row.stereotype if row.stereotype && !row.stereotype.empty?
|
|
81
|
+
refs
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Qea
|
|
6
|
+
# Translates EA t_connector rows into the appropriate
|
|
7
|
+
# Ea::Model::Relationship subclass. Dispatches on
|
|
8
|
+
# `Connector_Type` via ConnectorRelationshipMap.
|
|
9
|
+
class RelationshipBuilder
|
|
10
|
+
attr_reader :database
|
|
11
|
+
|
|
12
|
+
def initialize(database)
|
|
13
|
+
@database = database
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build_all
|
|
17
|
+
connectors = database.collections[:connectors] || []
|
|
18
|
+
connectors.filter_map { |row| build_one(row) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build_one(row)
|
|
22
|
+
klass = ConnectorRelationshipMap.class_for(row.connector_type)
|
|
23
|
+
args = common_args(row)
|
|
24
|
+
args.merge!(association_args(row)) if klass == Ea::Model::Association
|
|
25
|
+
klass.new(**args)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def common_args(row)
|
|
31
|
+
{
|
|
32
|
+
id: IdNormalizer.from_guid(row.ea_guid),
|
|
33
|
+
name: row.name,
|
|
34
|
+
qualified_name: row.name,
|
|
35
|
+
annotations: AnnotationBuilder.from_note(row.notes, row.ea_guid,
|
|
36
|
+
kind: "documentation")
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def association_args(row)
|
|
41
|
+
{
|
|
42
|
+
source_id: id_for_object_id(row.start_object_id),
|
|
43
|
+
target_id: id_for_object_id(row.end_object_id),
|
|
44
|
+
source_role_name: row.sourcerole,
|
|
45
|
+
target_role_name: row.destrole,
|
|
46
|
+
source_multiplicity_lower: parse_lower(row.sourcecard),
|
|
47
|
+
source_multiplicity_upper: parse_upper(row.sourcecard),
|
|
48
|
+
target_multiplicity_lower: parse_lower(row.destcard),
|
|
49
|
+
target_multiplicity_upper: parse_upper(row.destcard),
|
|
50
|
+
source_aggregation: ConnectorRelationshipMap
|
|
51
|
+
.source_aggregation_for(row.connector_type),
|
|
52
|
+
target_aggregation: aggregation_from(row.destaccess),
|
|
53
|
+
source_navigable: navigable?(row.sourceaccess, row.direction,
|
|
54
|
+
:source),
|
|
55
|
+
target_navigable: navigable?(row.destaccess, row.direction, :target)
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def id_for_object_id(ea_object_id)
|
|
60
|
+
obj = database.find_object(ea_object_id)
|
|
61
|
+
return nil unless obj
|
|
62
|
+
|
|
63
|
+
IdNormalizer.from_guid(obj.ea_guid)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def parse_lower(cardinality)
|
|
67
|
+
return 1 if cardinality.nil? || cardinality.empty?
|
|
68
|
+
|
|
69
|
+
# EA format: "1", "0..*", "1..1", etc. Take the first number.
|
|
70
|
+
begin
|
|
71
|
+
Integer(cardinality.split("..").first)
|
|
72
|
+
rescue StandardError
|
|
73
|
+
1
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def parse_upper(cardinality)
|
|
78
|
+
return 1 if cardinality.nil? || cardinality.empty?
|
|
79
|
+
|
|
80
|
+
upper = cardinality.split("..").last
|
|
81
|
+
return -1 if upper == "*"
|
|
82
|
+
|
|
83
|
+
begin
|
|
84
|
+
Integer(upper)
|
|
85
|
+
rescue StandardError
|
|
86
|
+
1
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def aggregation_from(access_value)
|
|
91
|
+
case access_value.to_s
|
|
92
|
+
when "0" then "none"
|
|
93
|
+
when "1" then "shared"
|
|
94
|
+
when "2" then "composite"
|
|
95
|
+
else "none"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def navigable?(_access, direction, end_label)
|
|
100
|
+
# EA's navigability is messy. Treat unspecified direction
|
|
101
|
+
# as bidirectional, "Source -> Destination" as target-only
|
|
102
|
+
# navigable, etc. Conservative default: both ends navigable.
|
|
103
|
+
case direction.to_s
|
|
104
|
+
when "<->", "" then true
|
|
105
|
+
when "->" then end_label == :target
|
|
106
|
+
when "<-" then end_label == :source
|
|
107
|
+
else true
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|