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,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