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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.refactor/01-ea-model-domain-layer.md +64 -0
  3. data/TODO.refactor/02-ea-model-qea-source-adapter.md +58 -0
  4. data/TODO.refactor/03-ea-model-xmi-source-adapter.md +33 -0
  5. data/TODO.refactor/04-ea-model-annotations-tagged-values.md +34 -0
  6. data/TODO.refactor/05-ea-model-diagrams-coordinates.md +37 -0
  7. data/TODO.refactor/06-spa-projection-layer.md +45 -0
  8. data/TODO.refactor/07-spa-sharded-output-strategy.md +28 -0
  9. data/TODO.refactor/08-spa-build-time-search-index.md +24 -0
  10. data/TODO.refactor/09-spa-frontend-data-contract.md +25 -0
  11. data/TODO.refactor/10-spa-cli-command-rework.md +34 -0
  12. data/TODO.refactor/11-spa-perf-benchmark.md +23 -0
  13. data/TODO.refactor/12-lutaml-uml-static-site-deprecation.md +28 -0
  14. data/lib/ea/cli/app.rb +13 -7
  15. data/lib/ea/cli/command/spa.rb +33 -48
  16. data/lib/ea/cli/command/svg.rb +68 -0
  17. data/lib/ea/cli/command.rb +1 -0
  18. data/lib/ea/model/annotation.rb +29 -0
  19. data/lib/ea/model/association.rb +40 -0
  20. data/lib/ea/model/base.rb +27 -0
  21. data/lib/ea/model/bounds.rb +22 -0
  22. data/lib/ea/model/classifier.rb +41 -0
  23. data/lib/ea/model/data_type.rb +9 -0
  24. data/lib/ea/model/dependency.rb +19 -0
  25. data/lib/ea/model/diagram.rb +28 -0
  26. data/lib/ea/model/diagram_connector.rb +30 -0
  27. data/lib/ea/model/diagram_element.rb +23 -0
  28. data/lib/ea/model/document.rb +107 -0
  29. data/lib/ea/model/enumeration.rb +15 -0
  30. data/lib/ea/model/enumeration_literal.rb +16 -0
  31. data/lib/ea/model/generalization.rb +19 -0
  32. data/lib/ea/model/interface.rb +9 -0
  33. data/lib/ea/model/klass.rb +16 -0
  34. data/lib/ea/model/metadata.rb +31 -0
  35. data/lib/ea/model/operation.rb +33 -0
  36. data/lib/ea/model/package.rb +36 -0
  37. data/lib/ea/model/parameter.rb +24 -0
  38. data/lib/ea/model/point.rb +16 -0
  39. data/lib/ea/model/polymorphic_maps.rb +30 -0
  40. data/lib/ea/model/primitive_type.rb +9 -0
  41. data/lib/ea/model/property.rb +49 -0
  42. data/lib/ea/model/realization.rb +19 -0
  43. data/lib/ea/model/relationship.rb +21 -0
  44. data/lib/ea/model/stereotype.rb +24 -0
  45. data/lib/ea/model/tagged_value.rb +20 -0
  46. data/lib/ea/model/waypoint.rb +19 -0
  47. data/lib/ea/model.rb +48 -0
  48. data/lib/ea/sources/qea/adapter.rb +63 -0
  49. data/lib/ea/sources/qea/annotation_builder.rb +36 -0
  50. data/lib/ea/sources/qea/classifier_builder.rb +119 -0
  51. data/lib/ea/sources/qea/connector_relationship_map.rb +42 -0
  52. data/lib/ea/sources/qea/diagram_builder.rb +214 -0
  53. data/lib/ea/sources/qea/diagram_style_parser.rb +43 -0
  54. data/lib/ea/sources/qea/id_normalizer.rb +24 -0
  55. data/lib/ea/sources/qea/metadata_builder.rb +42 -0
  56. data/lib/ea/sources/qea/object_classifier_map.rb +36 -0
  57. data/lib/ea/sources/qea/operation_builder.rb +79 -0
  58. data/lib/ea/sources/qea/package_builder.rb +44 -0
  59. data/lib/ea/sources/qea/property_builder.rb +86 -0
  60. data/lib/ea/sources/qea/relationship_builder.rb +113 -0
  61. data/lib/ea/sources/qea/stereotype_builder.rb +44 -0
  62. data/lib/ea/sources/qea/tagged_value_builder.rb +39 -0
  63. data/lib/ea/sources/qea.rb +31 -0
  64. data/lib/ea/sources/xmi/adapter.rb +62 -0
  65. data/lib/ea/sources/xmi/annotation_builder.rb +49 -0
  66. data/lib/ea/sources/xmi/classifier_builder.rb +89 -0
  67. data/lib/ea/sources/xmi/diagram_builder.rb +82 -0
  68. data/lib/ea/sources/xmi/id_normalizer.rb +27 -0
  69. data/lib/ea/sources/xmi/metadata_builder.rb +32 -0
  70. data/lib/ea/sources/xmi/operation_builder.rb +59 -0
  71. data/lib/ea/sources/xmi/package_builder.rb +73 -0
  72. data/lib/ea/sources/xmi/property_builder.rb +77 -0
  73. data/lib/ea/sources/xmi/relationship_builder.rb +142 -0
  74. data/lib/ea/sources/xmi/relationship_type_map.rb +34 -0
  75. data/lib/ea/sources/xmi/type_classifier_map.rb +33 -0
  76. data/lib/ea/sources/xmi.rb +29 -0
  77. data/lib/ea/sources.rb +15 -0
  78. data/lib/ea/spa/configuration.rb +110 -0
  79. data/lib/ea/spa/generator.rb +45 -0
  80. data/lib/ea/spa/lazy_ref.rb +18 -0
  81. data/lib/ea/spa/output/sharded_multi_file_strategy.rb +71 -0
  82. data/lib/ea/spa/output/single_file_strategy.rb +56 -0
  83. data/lib/ea/spa/output/strategy.rb +36 -0
  84. data/lib/ea/spa/output.rb +11 -0
  85. data/lib/ea/spa/package_tree.rb +15 -0
  86. data/lib/ea/spa/package_tree_node.rb +26 -0
  87. data/lib/ea/spa/projector.rb +199 -0
  88. data/lib/ea/spa/search_entry.rb +28 -0
  89. data/lib/ea/spa/search_index.rb +18 -0
  90. data/lib/ea/spa/shard.rb +19 -0
  91. data/lib/ea/spa/skeleton.rb +22 -0
  92. data/lib/ea/spa/skeleton_entry.rb +27 -0
  93. data/lib/ea/spa.rb +31 -0
  94. data/lib/ea/svg/bounds_calculator.rb +74 -0
  95. data/lib/ea/svg/connector_path.rb +138 -0
  96. data/lib/ea/svg/element_box.rb +256 -0
  97. data/lib/ea/svg/renderer.rb +77 -0
  98. data/lib/ea/svg/style_resolver.rb +72 -0
  99. data/lib/ea/svg.rb +21 -0
  100. data/lib/ea/version.rb +1 -1
  101. data/lib/ea.rb +4 -0
  102. 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