ea 0.2.4 → 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 +105 -4
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Sources
5
+ module Qea
6
+ # Parses EA's packed `objectstyle` / `style` / `styleex`
7
+ # strings into a Hash with typed keys. EA packs visual style
8
+ # as `DUID=...;BCol=...;FCol=...;` etc. — semicolon-separated
9
+ # key=value pairs with non-standard escaping.
10
+ module DiagramStyleParser
11
+ module_function
12
+
13
+ def parse(style_string)
14
+ return {} if style_string.nil? || style_string.empty?
15
+
16
+ style_string.to_s
17
+ .split(";")
18
+ .filter_map { |pair| parse_pair(pair) }
19
+ .to_h
20
+ end
21
+
22
+ def parse_pair(pair)
23
+ return nil unless pair.include?("=")
24
+
25
+ key, value = pair.split("=", 2)
26
+ return nil if key.nil? || key.empty?
27
+
28
+ [normalize_key(key), normalize_value(value)]
29
+ end
30
+
31
+ def normalize_key(key)
32
+ key.strip.downcase.to_sym
33
+ end
34
+
35
+ def normalize_value(value)
36
+ return "" if value.nil?
37
+
38
+ value.strip
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Sources
5
+ module Qea
6
+ # Normalizes EA's identifier formats into stable strings used
7
+ # as Ea::Model element ids. EA represents identity as
8
+ # `{GUID}` strings. We strip the braces to give the bare GUID.
9
+ module IdNormalizer
10
+ module_function
11
+
12
+ def from_guid(ea_guid)
13
+ return nil if ea_guid.nil? || ea_guid.empty?
14
+
15
+ ea_guid.to_s.gsub(/[{}]/, "")
16
+ end
17
+
18
+ def synthetic(prefix, *parts)
19
+ "#{prefix}:#{parts.join(":")}"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Sources
5
+ module Qea
6
+ # Builds an Ea::Model::Metadata from the QEA database. Pulls
7
+ # project-level info (model name, version, tool, source path)
8
+ # from the root package and the QEA file path.
9
+ class MetadataBuilder
10
+ attr_reader :database, :qea_path
11
+
12
+ def initialize(database, qea_path)
13
+ @database = database
14
+ @qea_path = qea_path
15
+ end
16
+
17
+ def build
18
+ root_pkg = root_package
19
+ Ea::Model::Metadata.new(
20
+ id: "metadata",
21
+ title: root_pkg&.name,
22
+ version: root_pkg&.version,
23
+ created_date: root_pkg&.createddate,
24
+ modified_date: root_pkg&.modifieddate,
25
+ author: root_pkg&.pkgowner,
26
+ source_format: "qea",
27
+ source_tool: "Sparx Enterprise Architect",
28
+ source_path: qea_path
29
+ )
30
+ end
31
+
32
+ private
33
+
34
+ def root_package
35
+ @root_package ||= database.collections[:packages]&.find do |pkg|
36
+ pkg.parent_id.nil? || pkg.parent_id.zero?
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Sources
5
+ module Qea
6
+ # Maps EA's `t_object.Object_Type` discriminator to the
7
+ # concrete Ea::Model::Classifier subclass that should be
8
+ # produced. New classifier kinds = one entry here, nothing
9
+ # else changes (OCP).
10
+ module ObjectClassifierMap
11
+ module_function
12
+
13
+ # EA Object_Type → Model class. Unknown types default to
14
+ # Klass — the most generic classifier.
15
+ LOOKUP = {
16
+ "Class" => "Ea::Model::Klass",
17
+ "DataType" => "Ea::Model::DataType",
18
+ "Enumeration" => "Ea::Model::Enumeration",
19
+ "Interface" => "Ea::Model::Interface",
20
+ "PrimitiveType" => "Ea::Model::PrimitiveType",
21
+ "Primitive" => "Ea::Model::PrimitiveType"
22
+ }.freeze
23
+
24
+ def class_for(object_type)
25
+ name = LOOKUP[object_type] || "Ea::Model::Klass"
26
+ Object.const_get(name)
27
+ end
28
+
29
+ def model_kind_for(object_type)
30
+ cls = class_for(object_type)
31
+ cls.new.model_kind
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Sources
5
+ module Qea
6
+ # Translates EA t_operation + t_operationparams rows into
7
+ # Ea::Model::Operation instances with owned Parameters.
8
+ class OperationBuilder
9
+ attr_reader :database
10
+
11
+ def initialize(database)
12
+ @database = database
13
+ end
14
+
15
+ def build_all_for(owner_object)
16
+ ops = database.operations_for_object(owner_object.ea_object_id) || []
17
+ ops.map { |row| build_one(row, owner_object) }
18
+ end
19
+
20
+ def build_one(row, owner_object)
21
+ Ea::Model::Operation.new(
22
+ id: IdNormalizer.from_guid(row.ea_guid),
23
+ name: row.name,
24
+ owner_id: IdNormalizer.from_guid(owner_object.ea_guid),
25
+ qualified_name: "#{owner_object.name}::#{row.name}",
26
+ return_type_name: row.type,
27
+ visibility: visibility_from_scope(row.scope),
28
+ is_static: boolean(row.isstatic),
29
+ is_abstract: boolean(row.abstract),
30
+ parameters: build_parameters(row),
31
+ annotations: AnnotationBuilder.from_note(row.notes, row.ea_guid,
32
+ kind: "documentation")
33
+ )
34
+ end
35
+
36
+ private
37
+
38
+ def build_parameters(operation_row)
39
+ params = database.operation_params_for(operation_row.operationid) || []
40
+ params.map.with_index do |p, idx|
41
+ Ea::Model::Parameter.new(
42
+ id: IdNormalizer.from_guid(p.ea_guid),
43
+ name: p.name,
44
+ ordinal: p.pos || idx,
45
+ direction: direction_from_kind(p.kind),
46
+ type_name: p.type,
47
+ default_value: p.default
48
+ )
49
+ end
50
+ end
51
+
52
+ def direction_from_kind(kind)
53
+ case kind.to_s
54
+ when "1", "out" then "out"
55
+ when "2", "inout" then "inout"
56
+ when "3", "return" then "return"
57
+ else "in"
58
+ end
59
+ end
60
+
61
+ def visibility_from_scope(scope)
62
+ case scope.to_s
63
+ when "Public" then "public"
64
+ when "Protected" then "protected"
65
+ when "Private" then "private"
66
+ else "public"
67
+ end
68
+ end
69
+
70
+ def boolean(value)
71
+ case value.to_s
72
+ when "1", "true", "TRUE" then true
73
+ else false
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Sources
5
+ module Qea
6
+ # Translates EA t_package rows into Ea::Model::Package
7
+ # instances. References to parent and child packages are by
8
+ # model id (normalized GUID), not by EA's integer Package_ID.
9
+ class PackageBuilder
10
+ attr_reader :database
11
+
12
+ def initialize(database)
13
+ @database = database
14
+ end
15
+
16
+ def build_all
17
+ packages = database.collections[:packages] || []
18
+ packages.map { |pkg| build_one(pkg) }
19
+ end
20
+
21
+ def build_one(pkg)
22
+ Ea::Model::Package.new(
23
+ id: IdNormalizer.from_guid(pkg.ea_guid),
24
+ name: pkg.name,
25
+ parent_id: parent_id_for(pkg),
26
+ qualified_name: pkg.xmlpath,
27
+ annotations: AnnotationBuilder.from_note(pkg.notes, pkg.ea_guid)
28
+ )
29
+ end
30
+
31
+ private
32
+
33
+ def parent_id_for(pkg)
34
+ return nil if pkg.parent_id.nil? || pkg.parent_id.zero?
35
+
36
+ parent = database.find_package(pkg.parent_id)
37
+ return nil unless parent
38
+
39
+ IdNormalizer.from_guid(parent.ea_guid)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Sources
5
+ module Qea
6
+ # Translates EA t_attribute rows into Ea::Model::Property
7
+ # instances. Multiplicity is parsed from EA's `LowerBound` /
8
+ # `UpperBound` columns (with `*` represented as -1 in the
9
+ # model).
10
+ class PropertyBuilder
11
+ attr_reader :database
12
+
13
+ def initialize(database)
14
+ @database = database
15
+ end
16
+
17
+ def build_all_for(owner_object)
18
+ attrs = database.attributes_for_object(owner_object.ea_object_id) || []
19
+ attrs.map { |row| build_one(row, owner_object) }
20
+ end
21
+
22
+ def build_one(row, owner_object)
23
+ Ea::Model::Property.new(
24
+ id: IdNormalizer.from_guid(row.ea_guid),
25
+ name: row.name,
26
+ owner_id: IdNormalizer.from_guid(owner_object.ea_guid),
27
+ type_name: row.type,
28
+ qualified_name: "#{owner_object.name}::#{row.name}",
29
+ multiplicity_lower: parse_lower(row),
30
+ multiplicity_upper: parse_upper(row),
31
+ default_value: row.default,
32
+ is_ordered: boolean(row.isordered),
33
+ is_unique: !boolean(row.allowduplicates),
34
+ visibility: visibility_from_scope(row.scope),
35
+ aggregation: "none",
36
+ stereotype_refs: stereotype_refs(row),
37
+ tagged_values: TaggedValueBuilder.new(database).for_attribute(row),
38
+ annotations: AnnotationBuilder.from_note(row.notes, row.ea_guid,
39
+ kind: "documentation")
40
+ )
41
+ end
42
+
43
+ private
44
+
45
+ def parse_lower(row)
46
+ Integer(row.lowerbound || 1)
47
+ rescue StandardError
48
+ 1
49
+ end
50
+
51
+ def parse_upper(row)
52
+ return -1 if row.upperbound.to_s == "*"
53
+
54
+ begin
55
+ Integer(row.upperbound || 1)
56
+ rescue StandardError
57
+ 1
58
+ end
59
+ end
60
+
61
+ def boolean(value)
62
+ case value.to_s
63
+ when "1", "true", "TRUE" then true
64
+ else false
65
+ end
66
+ end
67
+
68
+ def visibility_from_scope(scope)
69
+ case scope.to_s
70
+ when "Public" then "public"
71
+ when "Protected" then "protected"
72
+ when "Private" then "private"
73
+ when "Package" then "package"
74
+ else "public"
75
+ end
76
+ end
77
+
78
+ def stereotype_refs(row)
79
+ refs = []
80
+ refs << row.stereotype if row.stereotype && !row.stereotype.empty?
81
+ refs
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Sources
5
+ module Qea
6
+ # Translates EA t_connector rows into the appropriate
7
+ # Ea::Model::Relationship subclass. Dispatches on
8
+ # `Connector_Type` via ConnectorRelationshipMap.
9
+ class RelationshipBuilder
10
+ attr_reader :database
11
+
12
+ def initialize(database)
13
+ @database = database
14
+ end
15
+
16
+ def build_all
17
+ connectors = database.collections[:connectors] || []
18
+ connectors.filter_map { |row| build_one(row) }
19
+ end
20
+
21
+ def build_one(row)
22
+ klass = ConnectorRelationshipMap.class_for(row.connector_type)
23
+ args = common_args(row)
24
+ args.merge!(association_args(row)) if klass == Ea::Model::Association
25
+ klass.new(**args)
26
+ end
27
+
28
+ private
29
+
30
+ def common_args(row)
31
+ {
32
+ id: IdNormalizer.from_guid(row.ea_guid),
33
+ name: row.name,
34
+ qualified_name: row.name,
35
+ annotations: AnnotationBuilder.from_note(row.notes, row.ea_guid,
36
+ kind: "documentation")
37
+ }
38
+ end
39
+
40
+ def association_args(row)
41
+ {
42
+ source_id: id_for_object_id(row.start_object_id),
43
+ target_id: id_for_object_id(row.end_object_id),
44
+ source_role_name: row.sourcerole,
45
+ target_role_name: row.destrole,
46
+ source_multiplicity_lower: parse_lower(row.sourcecard),
47
+ source_multiplicity_upper: parse_upper(row.sourcecard),
48
+ target_multiplicity_lower: parse_lower(row.destcard),
49
+ target_multiplicity_upper: parse_upper(row.destcard),
50
+ source_aggregation: ConnectorRelationshipMap
51
+ .source_aggregation_for(row.connector_type),
52
+ target_aggregation: aggregation_from(row.destaccess),
53
+ source_navigable: navigable?(row.sourceaccess, row.direction,
54
+ :source),
55
+ target_navigable: navigable?(row.destaccess, row.direction, :target)
56
+ }
57
+ end
58
+
59
+ def id_for_object_id(ea_object_id)
60
+ obj = database.find_object(ea_object_id)
61
+ return nil unless obj
62
+
63
+ IdNormalizer.from_guid(obj.ea_guid)
64
+ end
65
+
66
+ def parse_lower(cardinality)
67
+ return 1 if cardinality.nil? || cardinality.empty?
68
+
69
+ # EA format: "1", "0..*", "1..1", etc. Take the first number.
70
+ begin
71
+ Integer(cardinality.split("..").first)
72
+ rescue StandardError
73
+ 1
74
+ end
75
+ end
76
+
77
+ def parse_upper(cardinality)
78
+ return 1 if cardinality.nil? || cardinality.empty?
79
+
80
+ upper = cardinality.split("..").last
81
+ return -1 if upper == "*"
82
+
83
+ begin
84
+ Integer(upper)
85
+ rescue StandardError
86
+ 1
87
+ end
88
+ end
89
+
90
+ def aggregation_from(access_value)
91
+ case access_value.to_s
92
+ when "0" then "none"
93
+ when "1" then "shared"
94
+ when "2" then "composite"
95
+ else "none"
96
+ end
97
+ end
98
+
99
+ def navigable?(_access, direction, end_label)
100
+ # EA's navigability is messy. Treat unspecified direction
101
+ # as bidirectional, "Source -> Destination" as target-only
102
+ # navigable, etc. Conservative default: both ends navigable.
103
+ case direction.to_s
104
+ when "<->", "" then true
105
+ when "->" then end_label == :target
106
+ when "<-" then end_label == :source
107
+ else true
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -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