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,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Qea
|
|
6
|
+
# Builds Ea::Model::Stereotype instances from EA's
|
|
7
|
+
# t_stereotypes table and from the `stereotype` column on
|
|
8
|
+
# individual elements. EA's stereotype storage is messy — a
|
|
9
|
+
# stereotype can be declared globally and applied per-element
|
|
10
|
+
# by name. We treat each distinct applied stereotype name as
|
|
11
|
+
# an instance and reference it from the applying element.
|
|
12
|
+
class StereotypeBuilder
|
|
13
|
+
attr_reader :database
|
|
14
|
+
|
|
15
|
+
def initialize(database)
|
|
16
|
+
@database = database
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def build_all
|
|
20
|
+
stereotypes = database.collections[:stereotypes] || []
|
|
21
|
+
stereotypes.map { |st| build_one(st) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def build_one(stereotype_row)
|
|
25
|
+
name = stereotype_row.stereotype
|
|
26
|
+
Ea::Model::Stereotype.new(
|
|
27
|
+
id: IdNormalizer.from_guid(stereotype_row.ea_guid) ||
|
|
28
|
+
IdNormalizer.synthetic("stereotype", name),
|
|
29
|
+
name: name,
|
|
30
|
+
qualified_name: name,
|
|
31
|
+
profile: stereotype_row.appliesto
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Apply references: which stereotypes apply to a given EA object.
|
|
36
|
+
def refs_for_object(object)
|
|
37
|
+
refs = []
|
|
38
|
+
refs << object.stereotype if object.stereotype && !object.stereotype.empty?
|
|
39
|
+
refs.uniq
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Qea
|
|
6
|
+
# Builds Ea::Model::TaggedValue instances from EA's
|
|
7
|
+
# t_objectproperties (and t_attributetags for attribute-level
|
|
8
|
+
# tagged values). Each row is one key/value pair; we keep the
|
|
9
|
+
# optional stereotype scope if EA recorded one.
|
|
10
|
+
class TaggedValueBuilder
|
|
11
|
+
attr_reader :database
|
|
12
|
+
|
|
13
|
+
def initialize(database)
|
|
14
|
+
@database = database
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def for_object(ea_guid)
|
|
18
|
+
rows = database.tagged_values_for_element(ea_guid) || []
|
|
19
|
+
rows.map { |row| build_from_row(row, ea_guid) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def for_attribute(attribute_row)
|
|
23
|
+
return [] unless attribute_row.respond_to?(:ea_guid)
|
|
24
|
+
|
|
25
|
+
rows = database.tagged_values_for_element(attribute_row.ea_guid) || []
|
|
26
|
+
rows.map { |row| build_from_row(row, attribute_row.ea_guid) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def build_from_row(row, owner_guid)
|
|
30
|
+
Ea::Model::TaggedValue.new(
|
|
31
|
+
id: IdNormalizer.synthetic("tv", owner_guid, row.property&.gsub(/\W+/, "_")),
|
|
32
|
+
key: row.property,
|
|
33
|
+
value: row.value
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
# QEA source adapter. Produces an Ea::Model::Document from a
|
|
6
|
+
# parsed Ea::Qea::Database by walking the SQLite-derived tables
|
|
7
|
+
# once and translating each row into a model instance.
|
|
8
|
+
#
|
|
9
|
+
# Structure: Adapter coordinates per-domain Builders. Each
|
|
10
|
+
# Builder owns one source table → model type translation, so
|
|
11
|
+
# new model types or new source columns only touch one Builder
|
|
12
|
+
# (OCP/MECE).
|
|
13
|
+
module Qea
|
|
14
|
+
autoload :IdNormalizer, "ea/sources/qea/id_normalizer"
|
|
15
|
+
autoload :ObjectClassifierMap, "ea/sources/qea/object_classifier_map"
|
|
16
|
+
autoload :ConnectorRelationshipMap, "ea/sources/qea/connector_relationship_map"
|
|
17
|
+
autoload :MetadataBuilder, "ea/sources/qea/metadata_builder"
|
|
18
|
+
autoload :PackageBuilder, "ea/sources/qea/package_builder"
|
|
19
|
+
autoload :ClassifierBuilder, "ea/sources/qea/classifier_builder"
|
|
20
|
+
autoload :PropertyBuilder, "ea/sources/qea/property_builder"
|
|
21
|
+
autoload :OperationBuilder, "ea/sources/qea/operation_builder"
|
|
22
|
+
autoload :RelationshipBuilder, "ea/sources/qea/relationship_builder"
|
|
23
|
+
autoload :StereotypeBuilder, "ea/sources/qea/stereotype_builder"
|
|
24
|
+
autoload :TaggedValueBuilder, "ea/sources/qea/tagged_value_builder"
|
|
25
|
+
autoload :AnnotationBuilder, "ea/sources/qea/annotation_builder"
|
|
26
|
+
autoload :DiagramBuilder, "ea/sources/qea/diagram_builder"
|
|
27
|
+
autoload :DiagramStyleParser, "ea/sources/qea/diagram_style_parser"
|
|
28
|
+
autoload :Adapter, "ea/sources/qea/adapter"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
# Driving port: produces an Ea::Model::Document from a parsed
|
|
7
|
+
# Xmi::Sparx::Root. Walks the UML model tree once and
|
|
8
|
+
# delegates to per-domain builders. The result is structurally
|
|
9
|
+
# identical to what the QEA adapter produces for the same
|
|
10
|
+
# conceptual source — that's the "single way" promise.
|
|
11
|
+
class Adapter
|
|
12
|
+
attr_reader :root, :xmi_path
|
|
13
|
+
|
|
14
|
+
def initialize(root, xmi_path = nil)
|
|
15
|
+
@root = root
|
|
16
|
+
@xmi_path = xmi_path
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Convenience: build a document directly from an .xmi file
|
|
20
|
+
# path. Parses via Xmi::Sparx::Root.parse_xml.
|
|
21
|
+
def self.from_path(xmi_path)
|
|
22
|
+
require "xmi"
|
|
23
|
+
root = ::Xmi::Sparx::Root.parse_xml(File.read(xmi_path))
|
|
24
|
+
new(root, xmi_path).to_document
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_document
|
|
28
|
+
Ea::Model::Document.new(
|
|
29
|
+
metadata: metadata,
|
|
30
|
+
packages: packages,
|
|
31
|
+
classifiers: classifiers,
|
|
32
|
+
relationships: relationships,
|
|
33
|
+
stereotypes: [],
|
|
34
|
+
diagrams: diagrams
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def metadata
|
|
41
|
+
@metadata ||= MetadataBuilder.new(root, xmi_path).build
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def packages
|
|
45
|
+
@packages ||= PackageBuilder.new(root).build_all
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def classifiers
|
|
49
|
+
@classifiers ||= ClassifierBuilder.new(root).build_all
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def relationships
|
|
53
|
+
@relationships ||= RelationshipBuilder.new(root).build_all
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def diagrams
|
|
57
|
+
@diagrams ||= DiagramBuilder.new(root).build_all
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -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
|