ea 0.2.5 → 0.3.0
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 +130 -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 +96 -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 +194 -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 +46 -0
- data/lib/ea/svg/element_box.rb +71 -0
- data/lib/ea/svg/renderer.rb +70 -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,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
# Builds Ea::Model::Annotation instances from XMI owned_comment
|
|
7
|
+
# elements. Each non-empty comment body becomes one Annotation.
|
|
8
|
+
class AnnotationBuilder
|
|
9
|
+
attr_reader :owner_id
|
|
10
|
+
|
|
11
|
+
def initialize(owner_id)
|
|
12
|
+
@owner_id = owner_id
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Extract comments from a source element. Only PackagedElement-
|
|
16
|
+
# derived types (Class, Package, Association, etc.) carry
|
|
17
|
+
# owned_comment in the XMI schema; attribute/operation/literal
|
|
18
|
+
# do not. We dispatch on type rather than poking at the
|
|
19
|
+
# method, so unknown element types just yield no annotations.
|
|
20
|
+
def self.from_element(element, owner_id, kind: "documentation")
|
|
21
|
+
comments = comments_on(element)
|
|
22
|
+
from_comments(comments, owner_id, kind: kind)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.from_comments(comments, owner_id, kind: "documentation")
|
|
26
|
+
Array(comments).map { |c| new(owner_id).build(kind, c.body) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.comments_on(element)
|
|
30
|
+
return [] unless element.is_a?(::Xmi::Uml::PackagedElement)
|
|
31
|
+
|
|
32
|
+
element.owned_comment
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def build(kind, body, author: nil, modified_date: nil)
|
|
36
|
+
return nil if body.nil? || body.empty?
|
|
37
|
+
|
|
38
|
+
Ea::Model::Annotation.new(
|
|
39
|
+
id: IdNormalizer.synthetic_id(owner_id, kind),
|
|
40
|
+
kind: kind,
|
|
41
|
+
body: body,
|
|
42
|
+
author: author,
|
|
43
|
+
modified_date: modified_date
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
# Translates XMI classifier elements (uml:Class, uml:DataType,
|
|
7
|
+
# uml:Enumeration, uml:Interface, uml:PrimitiveType) into the
|
|
8
|
+
# appropriate Ea::Model::Classifier subclass. Uses
|
|
9
|
+
# TypeClassifierMap for dispatch (OCP).
|
|
10
|
+
class ClassifierBuilder
|
|
11
|
+
attr_reader :root
|
|
12
|
+
|
|
13
|
+
def initialize(root)
|
|
14
|
+
@root = root
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build_all
|
|
18
|
+
classifiers = []
|
|
19
|
+
walk(root.model) do |element, parent_pkg|
|
|
20
|
+
next unless TypeClassifierMap.classifier_type?(element.type)
|
|
21
|
+
|
|
22
|
+
classifiers << build_one(element, parent_pkg)
|
|
23
|
+
end
|
|
24
|
+
classifiers
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def build_one(element, containing_package)
|
|
28
|
+
klass = TypeClassifierMap.class_for(element.type)
|
|
29
|
+
args = common_args(element, containing_package)
|
|
30
|
+
args.merge!(enumeration_args(element)) if klass == Ea::Model::Enumeration
|
|
31
|
+
klass.new(**args)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Depth-first walk of the packaged_element tree. Yields each
|
|
37
|
+
# element with its containing uml:Package (or nil at the
|
|
38
|
+
# model root).
|
|
39
|
+
def walk(element, parent_pkg = nil, &block)
|
|
40
|
+
yield element, parent_pkg if block
|
|
41
|
+
element.packaged_element.each do |inner|
|
|
42
|
+
new_parent = inner.type == "uml:Package" ? inner : parent_pkg
|
|
43
|
+
walk(inner, new_parent, &block)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def common_args(element, containing_package)
|
|
48
|
+
id = IdNormalizer.from_xmi_id(element.id)
|
|
49
|
+
{
|
|
50
|
+
id: id,
|
|
51
|
+
name: element.name,
|
|
52
|
+
package_id: containing_package && IdNormalizer.from_xmi_id(containing_package.id),
|
|
53
|
+
qualified_name: qualified_name(element, containing_package),
|
|
54
|
+
is_abstract: boolean(element.is_abstract),
|
|
55
|
+
visibility: element.visibility,
|
|
56
|
+
properties: PropertyBuilder.new.build_all_for(element),
|
|
57
|
+
operations: OperationBuilder.new.build_all_for(element),
|
|
58
|
+
annotations: AnnotationBuilder.from_element(element, id)
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def enumeration_args(element)
|
|
63
|
+
literals = element.owned_literal.map.with_index do |lit, idx|
|
|
64
|
+
Ea::Model::EnumerationLiteral.new(
|
|
65
|
+
id: IdNormalizer.from_xmi_id(lit.id) ||
|
|
66
|
+
IdNormalizer.synthetic_id(element.id, "literal", idx.to_s),
|
|
67
|
+
name: lit.name,
|
|
68
|
+
value: lit.name,
|
|
69
|
+
ordinal: idx
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
{ literals: literals }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def qualified_name(element, containing_package)
|
|
76
|
+
pkg_name = containing_package&.name
|
|
77
|
+
pkg_name ? "#{pkg_name}::#{element.name}" : element.name
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def boolean(value)
|
|
81
|
+
case value
|
|
82
|
+
when true, "true" then true
|
|
83
|
+
else false
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
# Translates XMI uml:Diagram elements into Ea::Model::Diagram
|
|
7
|
+
# instances. Coordinates come from the UML-DI owned_element
|
|
8
|
+
# bounds; element references come from the model_element
|
|
9
|
+
# attribute.
|
|
10
|
+
class DiagramBuilder
|
|
11
|
+
attr_reader :root
|
|
12
|
+
|
|
13
|
+
def initialize(root)
|
|
14
|
+
@root = root
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build_all
|
|
18
|
+
diagrams = Array(root.model.diagram)
|
|
19
|
+
diagrams.map { |d| build_one(d) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build_one(diagram)
|
|
23
|
+
id = IdNormalizer.from_xmi_id(diagram.id)
|
|
24
|
+
ext_diagram = extension_diagram_for(id)
|
|
25
|
+
Ea::Model::Diagram.new(
|
|
26
|
+
id: id,
|
|
27
|
+
name: ext_diagram&.properties&.name,
|
|
28
|
+
package_id: ext_diagram&.model&.package,
|
|
29
|
+
diagram_type: ext_diagram&.properties&.type&.split(":")&.last&.downcase,
|
|
30
|
+
bounds: bounds_for(diagram),
|
|
31
|
+
elements: build_elements(diagram, id),
|
|
32
|
+
connectors: [],
|
|
33
|
+
annotations: AnnotationBuilder.from_element(diagram, id)
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def extension_diagram_for(diagram_id)
|
|
40
|
+
return nil unless root.extension&.diagrams
|
|
41
|
+
|
|
42
|
+
root.extension.diagrams.diagram.find { |d| d.id == diagram_id }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def bounds_for(diagram)
|
|
46
|
+
first_bounds = diagram.owned_element.flat_map(&:bounds).first
|
|
47
|
+
return nil unless first_bounds
|
|
48
|
+
|
|
49
|
+
Ea::Model::Bounds.new(
|
|
50
|
+
x: first_bounds.x || 0,
|
|
51
|
+
y: first_bounds.y || 0,
|
|
52
|
+
width: first_bounds.width || 0,
|
|
53
|
+
height: first_bounds.height || 0
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def build_elements(diagram, owner_diagram_id)
|
|
58
|
+
diagram.owned_element.flat_map do |oe|
|
|
59
|
+
oe.bounds.each_with_index.map do |bnd, idx|
|
|
60
|
+
build_element(oe, bnd, owner_diagram_id, idx)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def build_element(owned_element, bounds, diagram_id, idx)
|
|
66
|
+
Ea::Model::DiagramElement.new(
|
|
67
|
+
id: IdNormalizer.synthetic_id(diagram_id, "elem", idx.to_s),
|
|
68
|
+
diagram_id: diagram_id,
|
|
69
|
+
model_element_ref: IdNormalizer.from_xmi_id(owned_element.model_element),
|
|
70
|
+
bounds: Ea::Model::Bounds.new(
|
|
71
|
+
x: bounds.x || 0,
|
|
72
|
+
y: bounds.y || 0,
|
|
73
|
+
width: bounds.width || 0,
|
|
74
|
+
height: bounds.height || 0
|
|
75
|
+
),
|
|
76
|
+
style: {}
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
# Normalizes XMI identifiers (xmi:id strings, often EAID_...
|
|
7
|
+
# or GUID-shaped) into stable model ids. Pass-through for
|
|
8
|
+
# real ids; synthetic ids for derived elements that have no
|
|
9
|
+
# xmi:id (annotations, waypoints).
|
|
10
|
+
module IdNormalizer
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def from_xmi_id(xmi_id)
|
|
14
|
+
return nil if xmi_id.nil? || xmi_id.empty?
|
|
15
|
+
|
|
16
|
+
xmi_id.to_s
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def synthetic_id(owner_id, kind, suffix = nil)
|
|
20
|
+
parts = ["#{owner_id}:#{kind}"]
|
|
21
|
+
parts << suffix if suffix
|
|
22
|
+
parts.join(":")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
class MetadataBuilder
|
|
7
|
+
attr_reader :root, :xmi_path
|
|
8
|
+
|
|
9
|
+
def initialize(root, xmi_path)
|
|
10
|
+
@root = root
|
|
11
|
+
@xmi_path = xmi_path
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def build
|
|
15
|
+
model = root.model
|
|
16
|
+
documentation = root.documentation
|
|
17
|
+
Ea::Model::Metadata.new(
|
|
18
|
+
id: "metadata",
|
|
19
|
+
title: model&.name,
|
|
20
|
+
version: documentation&.exporter_version,
|
|
21
|
+
author: documentation&.exporter,
|
|
22
|
+
created_date: nil,
|
|
23
|
+
modified_date: documentation&.timestamp&.first&.iso8601,
|
|
24
|
+
source_format: "xmi",
|
|
25
|
+
source_tool: documentation&.exporter,
|
|
26
|
+
source_path: xmi_path
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
# Translates XMI OwnedOperation elements (with their
|
|
7
|
+
# OwnedParameter children) into Ea::Model::Operation instances.
|
|
8
|
+
class OperationBuilder
|
|
9
|
+
def build_all_for(classifier)
|
|
10
|
+
classifier.owned_operation.map { |op| build_one(op, classifier) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def build_one(op, owner)
|
|
14
|
+
id = IdNormalizer.from_xmi_id(op.id) ||
|
|
15
|
+
IdNormalizer.synthetic_id(owner.id, "op", op.name)
|
|
16
|
+
Ea::Model::Operation.new(
|
|
17
|
+
id: id,
|
|
18
|
+
name: op.name,
|
|
19
|
+
owner_id: owner.id,
|
|
20
|
+
qualified_name: "#{owner.name}::#{op.name}",
|
|
21
|
+
return_type_name: return_type_name_for(op),
|
|
22
|
+
visibility: op.visibility,
|
|
23
|
+
is_static: boolean(op.is_static),
|
|
24
|
+
is_abstract: boolean(op.is_abstract),
|
|
25
|
+
parameters: build_parameters(op, id),
|
|
26
|
+
annotations: AnnotationBuilder.from_element(op, id)
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def build_parameters(op, owner_op_id)
|
|
33
|
+
op.owned_parameter.map.with_index do |param, idx|
|
|
34
|
+
direction = param.direction || (idx.zero? ? "return" : "in")
|
|
35
|
+
Ea::Model::Parameter.new(
|
|
36
|
+
id: IdNormalizer.from_xmi_id(param.id) ||
|
|
37
|
+
IdNormalizer.synthetic_id(owner_op_id, "param", idx.to_s),
|
|
38
|
+
name: param.name || direction,
|
|
39
|
+
ordinal: idx,
|
|
40
|
+
direction: direction,
|
|
41
|
+
type_name: param.type
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def return_type_name_for(op)
|
|
47
|
+
op.owned_parameter.find { |p| p.direction == "return" }&.type
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def boolean(value)
|
|
51
|
+
case value
|
|
52
|
+
when true, "true" then true
|
|
53
|
+
else false
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
# Translates XMI uml:Package elements into Ea::Model::Package
|
|
7
|
+
# instances. Walks the packaged_element tree recursively so
|
|
8
|
+
# nested packages are picked up.
|
|
9
|
+
class PackageBuilder
|
|
10
|
+
attr_reader :root
|
|
11
|
+
|
|
12
|
+
def initialize(root)
|
|
13
|
+
@root = root
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build_all
|
|
17
|
+
packages = []
|
|
18
|
+
walk_packages(root.model, nil) do |pe, parent_id|
|
|
19
|
+
packages << build_one(pe, parent_id: parent_id)
|
|
20
|
+
end
|
|
21
|
+
packages
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def walk_packages(element, parent_id, &block)
|
|
25
|
+
element.packaged_element.each do |inner|
|
|
26
|
+
next unless inner.type == "uml:Package"
|
|
27
|
+
|
|
28
|
+
id = IdNormalizer.from_xmi_id(inner.id)
|
|
29
|
+
yield inner, parent_id
|
|
30
|
+
walk_packages(inner, id, &block)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def build_one(packaged_element, parent_id:)
|
|
35
|
+
id = IdNormalizer.from_xmi_id(packaged_element.id)
|
|
36
|
+
Ea::Model::Package.new(
|
|
37
|
+
id: id,
|
|
38
|
+
name: packaged_element.name,
|
|
39
|
+
parent_id: parent_id,
|
|
40
|
+
qualified_name: packaged_element.name,
|
|
41
|
+
sub_package_ids: child_package_ids(packaged_element),
|
|
42
|
+
classifier_ids: child_classifier_ids(packaged_element),
|
|
43
|
+
annotations: AnnotationBuilder.from_element(packaged_element, id)
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def packages_for(model_element)
|
|
50
|
+
return [] unless model_element
|
|
51
|
+
|
|
52
|
+
model_element.packaged_element.select { |pe| pe.type == "uml:Package" }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def child_package_ids(packaged_element)
|
|
56
|
+
packaged_element.packaged_element
|
|
57
|
+
.select { |pe| pe.type == "uml:Package" }
|
|
58
|
+
.map { |pe| IdNormalizer.from_xmi_id(pe.id) }
|
|
59
|
+
.compact
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def child_classifier_ids(packaged_element)
|
|
63
|
+
packaged_element.packaged_element
|
|
64
|
+
.filter_map do |pe|
|
|
65
|
+
next unless TypeClassifierMap.classifier_type?(pe.type)
|
|
66
|
+
|
|
67
|
+
IdNormalizer.from_xmi_id(pe.id)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
# Translates XMI OwnedAttribute elements into Ea::Model::Property
|
|
7
|
+
# instances. Multiplicity comes from lower_value / upper_value
|
|
8
|
+
# (LiteralInteger / LiteralUnlimitedNatural polymorphic
|
|
9
|
+
# children).
|
|
10
|
+
class PropertyBuilder
|
|
11
|
+
def build_all_for(classifier)
|
|
12
|
+
classifier.owned_attribute.map { |attr| build_one(attr, classifier) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build_one(attr, owner)
|
|
16
|
+
id = IdNormalizer.from_xmi_id(attr.id) ||
|
|
17
|
+
IdNormalizer.synthetic_id(owner.id, "attr", attr.name)
|
|
18
|
+
Ea::Model::Property.new(
|
|
19
|
+
id: id,
|
|
20
|
+
name: attr.name,
|
|
21
|
+
owner_id: owner.id,
|
|
22
|
+
type_name: type_name_for(attr),
|
|
23
|
+
qualified_name: "#{owner.name}::#{attr.name}",
|
|
24
|
+
multiplicity_lower: parse_lower(attr),
|
|
25
|
+
multiplicity_upper: parse_upper(attr),
|
|
26
|
+
default_value: default_value_for(attr),
|
|
27
|
+
is_derived: boolean(attr.is_derived),
|
|
28
|
+
is_readonly: boolean(attr.is_read_only),
|
|
29
|
+
is_ordered: boolean(attr.is_ordered),
|
|
30
|
+
is_unique: boolean(attr.is_unique),
|
|
31
|
+
aggregation: attr.aggregation || "none",
|
|
32
|
+
visibility: attr.visibility,
|
|
33
|
+
annotations: AnnotationBuilder.from_element(attr, id)
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def type_name_for(attr)
|
|
40
|
+
# OwnedAttribute#type is the xmi:type discriminator
|
|
41
|
+
# (e.g. "uml:Property"); the actual data type reference is
|
|
42
|
+
# on the child <type xmi:idref=...> element, exposed via
|
|
43
|
+
# uml_type.idref.
|
|
44
|
+
attr.uml_type&.idref
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def parse_lower(attr)
|
|
48
|
+
value = attr.lower_value
|
|
49
|
+
return 1 unless value
|
|
50
|
+
|
|
51
|
+
Integer(value.value) rescue 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def parse_upper(attr)
|
|
55
|
+
value = attr.upper_value
|
|
56
|
+
return 1 unless value
|
|
57
|
+
|
|
58
|
+
raw = value.value
|
|
59
|
+
return -1 if raw.to_s == "*"
|
|
60
|
+
|
|
61
|
+
Integer(raw) rescue 1
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def default_value_for(attr)
|
|
65
|
+
attr.default_value&.value
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def boolean(value)
|
|
69
|
+
case value
|
|
70
|
+
when true, "true" then true
|
|
71
|
+
else false
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
# Translates XMI relationship elements (uml:Association,
|
|
7
|
+
# uml:Generalization, uml:Realization, uml:Dependency) into
|
|
8
|
+
# the appropriate Ea::Model::Relationship subclass.
|
|
9
|
+
class RelationshipBuilder
|
|
10
|
+
attr_reader :root
|
|
11
|
+
|
|
12
|
+
def initialize(root)
|
|
13
|
+
@root = root
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build_all
|
|
17
|
+
relationships = []
|
|
18
|
+
walk(root.model) do |element|
|
|
19
|
+
next unless RelationshipTypeMap.relationship_type?(element.type)
|
|
20
|
+
|
|
21
|
+
rel = build_one(element)
|
|
22
|
+
relationships << rel if rel
|
|
23
|
+
end
|
|
24
|
+
relationships
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def build_one(element)
|
|
28
|
+
klass = RelationshipTypeMap.class_for(element.type)
|
|
29
|
+
args = common_args(element)
|
|
30
|
+
args.merge!(specific_args_for(klass, element))
|
|
31
|
+
klass.new(**args)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def walk(element, &block)
|
|
37
|
+
yield element if block
|
|
38
|
+
element.packaged_element.each { |inner| walk(inner, &block) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def common_args(element)
|
|
42
|
+
id = IdNormalizer.from_xmi_id(element.id)
|
|
43
|
+
{
|
|
44
|
+
id: id,
|
|
45
|
+
name: element.name || element.type&.split(":")&.last&.downcase,
|
|
46
|
+
qualified_name: element.name,
|
|
47
|
+
annotations: AnnotationBuilder.from_element(element, id)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def specific_args_for(klass, element)
|
|
52
|
+
case klass.name
|
|
53
|
+
when "Ea::Model::Association" then association_args(element)
|
|
54
|
+
when "Ea::Model::Generalization" then generalization_args(element)
|
|
55
|
+
when "Ea::Model::Realization" then realization_args(element)
|
|
56
|
+
when "Ea::Model::Dependency" then dependency_args(element)
|
|
57
|
+
else {}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def association_args(element)
|
|
62
|
+
ends = element.member_ends.to_a
|
|
63
|
+
source_id = ends.first&.idref
|
|
64
|
+
target_id = ends.last&.idref
|
|
65
|
+
owned_ends = element.owned_end.to_a
|
|
66
|
+
source_end = owned_ends.first
|
|
67
|
+
target_end = owned_ends.last
|
|
68
|
+
{
|
|
69
|
+
source_id: source_id,
|
|
70
|
+
target_id: target_id,
|
|
71
|
+
source_role_name: source_end&.name,
|
|
72
|
+
target_role_name: target_end&.name,
|
|
73
|
+
source_multiplicity_lower: parse_lower(source_end),
|
|
74
|
+
source_multiplicity_upper: parse_upper(source_end),
|
|
75
|
+
target_multiplicity_lower: parse_lower(target_end),
|
|
76
|
+
target_multiplicity_upper: parse_upper(target_end),
|
|
77
|
+
source_aggregation: source_end&.aggregation || "none",
|
|
78
|
+
target_aggregation: target_end&.aggregation || "none",
|
|
79
|
+
source_navigable: navigable?(source_end),
|
|
80
|
+
target_navigable: navigable?(target_end)
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def generalization_args(element)
|
|
85
|
+
# PackagedElement#generalization is a collection of
|
|
86
|
+
# AssociationGeneralization rows. The "specific" (child)
|
|
87
|
+
# is the owning element; "general" (parent) is the
|
|
88
|
+
# referenced id on each generalization row.
|
|
89
|
+
gen = element.generalization.first
|
|
90
|
+
{
|
|
91
|
+
specific_id: IdNormalizer.from_xmi_id(element.id),
|
|
92
|
+
general_id: gen&.general
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def realization_args(element)
|
|
97
|
+
{
|
|
98
|
+
realizing_id: element.client,
|
|
99
|
+
contract_id: element.supplier
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def dependency_args(element)
|
|
104
|
+
{
|
|
105
|
+
client_id: element.client,
|
|
106
|
+
supplier_id: element.supplier
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def parse_lower(end_element)
|
|
111
|
+
return 1 unless end_element
|
|
112
|
+
|
|
113
|
+
value = end_element.lower_value
|
|
114
|
+
return 1 unless value
|
|
115
|
+
|
|
116
|
+
Integer(value.value) rescue 1
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def parse_upper(end_element)
|
|
120
|
+
return 1 unless end_element
|
|
121
|
+
|
|
122
|
+
value = end_element.upper_value
|
|
123
|
+
return 1 unless value
|
|
124
|
+
|
|
125
|
+
raw = value.value
|
|
126
|
+
return -1 if raw.to_s == "*"
|
|
127
|
+
|
|
128
|
+
Integer(raw) rescue 1
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def navigable?(end_element)
|
|
132
|
+
return true unless end_element
|
|
133
|
+
|
|
134
|
+
# XMI represents non-navigable ends with aggregation=none
|
|
135
|
+
# and no role name; treat presence of role name or any
|
|
136
|
+
# aggregation as navigable. Default: navigable.
|
|
137
|
+
true
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
# Maps XMI's `xmi:type` discriminator for relationship-shaped
|
|
7
|
+
# packaged elements (uml:Association, uml:Generalization,
|
|
8
|
+
# etc.) to the concrete Ea::Model::Relationship subclass.
|
|
9
|
+
module RelationshipTypeMap
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
LOOKUP = {
|
|
13
|
+
"uml:Association" => "Ea::Model::Association",
|
|
14
|
+
"uml:Generalization" => "Ea::Model::Generalization",
|
|
15
|
+
"uml:Realization" => "Ea::Model::Realization",
|
|
16
|
+
"uml:Dependency" => "Ea::Model::Dependency",
|
|
17
|
+
"uml:Usage" => "Ea::Model::Dependency",
|
|
18
|
+
"uml:Abstraction" => "Ea::Model::Dependency",
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def class_for(xmi_type)
|
|
22
|
+
name = LOOKUP[xmi_type]
|
|
23
|
+
return nil unless name
|
|
24
|
+
|
|
25
|
+
Object.const_get(name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def relationship_type?(xmi_type)
|
|
29
|
+
LOOKUP.key?(xmi_type)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|