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.
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 +130 -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 +96 -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 +194 -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 +46 -0
  96. data/lib/ea/svg/element_box.rb +71 -0
  97. data/lib/ea/svg/renderer.rb +70 -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,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