ea 0.1.0 → 0.1.4

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 (227) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +125 -0
  3. data/Rakefile +12 -4
  4. data/TODO.next/00-publish-blocking-bugs.md +74 -0
  5. data/TODO.next/01-standalone-ea-gem-identity.md +76 -0
  6. data/TODO.next/02-optional-lutaml-uml-dependency.md +47 -0
  7. data/TODO.next/03-slim-lutaml-uml.md +79 -0
  8. data/TODO.next/04-loader-registry-for-uml-repository.md +49 -0
  9. data/TODO.next/05-extract-shared-transformer-methods.md +14 -0
  10. data/TODO.next/06-deduplicate-stereotype-loading.md +17 -0
  11. data/TODO.next/07-transformer-registry-in-factory.md +20 -0
  12. data/TODO.next/08-connector-type-registry.md +27 -0
  13. data/TODO.next/09-element-renderer-registry.md +29 -0
  14. data/TODO.next/10-connector-renderer-lsp.md +18 -0
  15. data/TODO.next/11-consolidate-style-knowledge.md +33 -0
  16. data/TODO.next/12-data-driven-from-db-row.md +24 -0
  17. data/TODO.next/13-extract-duplicated-methods.md +17 -0
  18. data/TODO.next/14-remove-dead-code.md +10 -0
  19. data/TODO.next/15-narrow-exception-handling.md +39 -0
  20. data/TODO.next/16-repository-indexes.md +28 -0
  21. data/TODO.next/17-fix-spec-quality-and-coverage.md +32 -0
  22. data/TODO.next/18-xmi-tool-specific-parser-architecture.md +172 -0
  23. data/TODO.next/19-fix-ea-gemspec-dependency-declarations.md +56 -0
  24. data/TODO.next/20-ci-requires-unreleased-lutaml-uml.md +63 -0
  25. data/TODO.next/21-qeatoxmi-via-xmi-gem.md +340 -0
  26. data/TODO.next/22-strip-respond-to-from-qeatoxmi-specs.md +32 -0
  27. data/TODO.next/23-cleanup-idallocator.md +41 -0
  28. data/TODO.next/24-tighten-parity-specs.md +42 -0
  29. data/TODO.next/25-sparx-eaid-format-for-synthesized-ids.md +62 -0
  30. data/TODO.next/26-fix-uppervalue-lowervalue-count-gap.md +51 -0
  31. data/TODO.next/27-extract-cardinality-module.md +68 -0
  32. data/TODO.next/28-extract-xml-sanitizer.md +51 -0
  33. data/TODO.next/29-ocp-registry-for-classifier-builders.md +58 -0
  34. data/TODO.next/30-struct-return-for-association-end.md +37 -0
  35. data/TODO.next/31-idallocator-specs.md +27 -0
  36. data/TODO.next/32-phase2-gap-sentinel-specs.md +53 -0
  37. data/TODO.next/33-normalize-lower-cleanup.md +30 -0
  38. data/TODO.next/34-document-member-end-order-rt-prefix.md +29 -0
  39. data/TODO.next/35-walk-runstate-for-instance-slots.md +76 -0
  40. data/TODO.next/36-wire-interface-realization.md +50 -0
  41. data/TODO.next/37-visibility-returns-real-booleans.md +36 -0
  42. data/config/diagram_styles.yml +200 -0
  43. data/config/model_transformations.yml +266 -0
  44. data/config/qea_schema.yml +1024 -0
  45. data/docs/ea_to_uml_type_mapping.md +89 -0
  46. data/docs/xmi_qea_conversion_capabilities.md +99 -0
  47. data/examples/lur/20251010_current_plateau_v5.1.lur +0 -0
  48. data/examples/lur/basic.lur +0 -0
  49. data/examples/lur/test-output.lur +0 -0
  50. data/examples/lur/test.lur +0 -0
  51. data/examples/lur_basic_usage.rb +221 -0
  52. data/examples/lur_cli_workflow.rb +263 -0
  53. data/examples/lur_statistics.rb +326 -0
  54. data/examples/qea/20251010_current_plateau_v5.1.qea +0 -0
  55. data/examples/qea/ArcGISWorkspace_template.qea +0 -0
  56. data/examples/qea/README_qea_parser.adoc +230 -0
  57. data/examples/qea/UmlModel_template.qea +0 -0
  58. data/examples/qea/basic.qea +0 -0
  59. data/examples/qea/simple.qea +0 -0
  60. data/examples/qea/simple_example.qea +0 -0
  61. data/examples/qea/test.qea +0 -0
  62. data/examples/qea_standalone_query.rb +73 -0
  63. data/examples/qea_to_repository.rb +51 -0
  64. data/examples/smoke_test_real_qea.rb +81 -0
  65. data/exe/ea +7 -0
  66. data/lib/ea/cli/app.rb +72 -0
  67. data/lib/ea/cli/command/base.rb +80 -0
  68. data/lib/ea/cli/command/convert.rb +62 -0
  69. data/lib/ea/cli/command/diagrams.rb +81 -0
  70. data/lib/ea/cli/command/list.rb +61 -0
  71. data/lib/ea/cli/command/parse.rb +29 -0
  72. data/lib/ea/cli/command/stats.rb +20 -0
  73. data/lib/ea/cli/command/validate.rb +41 -0
  74. data/lib/ea/cli/command.rb +15 -0
  75. data/lib/ea/cli/error.rb +34 -0
  76. data/lib/ea/cli/output/formatter.rb +34 -0
  77. data/lib/ea/cli/output/json_formatter.rb +20 -0
  78. data/lib/ea/cli/output/table_formatter.rb +42 -0
  79. data/lib/ea/cli/output/yaml_formatter.rb +20 -0
  80. data/lib/ea/cli/output.rb +56 -0
  81. data/lib/ea/cli.rb +17 -0
  82. data/lib/ea/diagram/configuration.rb +379 -0
  83. data/lib/ea/diagram/element_renderers/base_renderer.rb +77 -0
  84. data/lib/ea/diagram/element_renderers/class_renderer.rb +323 -0
  85. data/lib/ea/diagram/element_renderers/connector_renderer.rb +41 -0
  86. data/lib/ea/diagram/element_renderers/package_renderer.rb +61 -0
  87. data/lib/ea/diagram/element_renderers.rb +43 -0
  88. data/lib/ea/diagram/extractor.rb +560 -0
  89. data/lib/ea/diagram/layout_engine.rb +170 -0
  90. data/lib/ea/diagram/path_builder.rb +202 -0
  91. data/lib/ea/diagram/style_parser.rb +42 -0
  92. data/lib/ea/diagram/style_resolver.rb +276 -0
  93. data/lib/ea/diagram/svg_renderer.rb +274 -0
  94. data/lib/ea/diagram/util.rb +73 -0
  95. data/lib/ea/diagram.rb +47 -0
  96. data/lib/ea/qea/benchmark.rb +210 -0
  97. data/lib/ea/qea/database.rb +308 -0
  98. data/lib/ea/qea/factory/association_builder.rb +203 -0
  99. data/lib/ea/qea/factory/association_transformer.rb +91 -0
  100. data/lib/ea/qea/factory/attribute_tag_transformer.rb +57 -0
  101. data/lib/ea/qea/factory/attribute_transformer.rb +93 -0
  102. data/lib/ea/qea/factory/base_transformer.rb +177 -0
  103. data/lib/ea/qea/factory/class_transformer.rb +116 -0
  104. data/lib/ea/qea/factory/constraint_transformer.rb +75 -0
  105. data/lib/ea/qea/factory/data_type_transformer.rb +77 -0
  106. data/lib/ea/qea/factory/diagram_transformer.rb +157 -0
  107. data/lib/ea/qea/factory/document_builder.rb +283 -0
  108. data/lib/ea/qea/factory/ea_to_uml_factory.rb +229 -0
  109. data/lib/ea/qea/factory/enum_transformer.rb +74 -0
  110. data/lib/ea/qea/factory/generalization_builder.rb +227 -0
  111. data/lib/ea/qea/factory/generalization_transformer.rb +98 -0
  112. data/lib/ea/qea/factory/instance_transformer.rb +68 -0
  113. data/lib/ea/qea/factory/object_property_transformer.rb +58 -0
  114. data/lib/ea/qea/factory/operation_transformer.rb +66 -0
  115. data/lib/ea/qea/factory/package_transformer.rb +145 -0
  116. data/lib/ea/qea/factory/reference_resolver.rb +99 -0
  117. data/lib/ea/qea/factory/stereotype_loader.rb +39 -0
  118. data/lib/ea/qea/factory/tagged_value_transformer.rb +38 -0
  119. data/lib/ea/qea/factory/transformer_registry.rb +80 -0
  120. data/lib/ea/qea/factory.rb +37 -0
  121. data/lib/ea/qea/file_detector.rb +178 -0
  122. data/lib/ea/qea/infrastructure/database_connection.rb +100 -0
  123. data/lib/ea/qea/infrastructure/schema_reader.rb +136 -0
  124. data/lib/ea/qea/infrastructure/table_reader.rb +224 -0
  125. data/lib/ea/qea/infrastructure.rb +12 -0
  126. data/lib/ea/qea/models/base_model.rb +59 -0
  127. data/lib/ea/qea/models/ea_attribute.rb +109 -0
  128. data/lib/ea/qea/models/ea_attribute_tag.rb +100 -0
  129. data/lib/ea/qea/models/ea_complexity_type.rb +79 -0
  130. data/lib/ea/qea/models/ea_connector.rb +160 -0
  131. data/lib/ea/qea/models/ea_connector_type.rb +60 -0
  132. data/lib/ea/qea/models/ea_constraint_type.rb +63 -0
  133. data/lib/ea/qea/models/ea_datatype.rb +104 -0
  134. data/lib/ea/qea/models/ea_diagram.rb +115 -0
  135. data/lib/ea/qea/models/ea_diagram_link.rb +78 -0
  136. data/lib/ea/qea/models/ea_diagram_object.rb +73 -0
  137. data/lib/ea/qea/models/ea_diagram_type.rb +56 -0
  138. data/lib/ea/qea/models/ea_document.rb +63 -0
  139. data/lib/ea/qea/models/ea_object.rb +223 -0
  140. data/lib/ea/qea/models/ea_object_constraint.rb +53 -0
  141. data/lib/ea/qea/models/ea_object_property.rb +87 -0
  142. data/lib/ea/qea/models/ea_object_type.rb +73 -0
  143. data/lib/ea/qea/models/ea_operation.rb +127 -0
  144. data/lib/ea/qea/models/ea_operation_param.rb +76 -0
  145. data/lib/ea/qea/models/ea_package.rb +78 -0
  146. data/lib/ea/qea/models/ea_script.rb +62 -0
  147. data/lib/ea/qea/models/ea_status_type.rb +66 -0
  148. data/lib/ea/qea/models/ea_stereotype.rb +57 -0
  149. data/lib/ea/qea/models/ea_tagged_value.rb +99 -0
  150. data/lib/ea/qea/models/ea_xref.rb +165 -0
  151. data/lib/ea/qea/models.rb +35 -0
  152. data/lib/ea/qea/repositories/base_repository.rb +225 -0
  153. data/lib/ea/qea/repositories/object_repository.rb +219 -0
  154. data/lib/ea/qea/repositories.rb +10 -0
  155. data/lib/ea/qea/services/configuration.rb +211 -0
  156. data/lib/ea/qea/services/database_loader.rb +191 -0
  157. data/lib/ea/qea/services.rb +10 -0
  158. data/lib/ea/qea/validation/association_validator.rb +73 -0
  159. data/lib/ea/qea/validation/attribute_validator.rb +91 -0
  160. data/lib/ea/qea/validation/base_validator.rb +331 -0
  161. data/lib/ea/qea/validation/class_validator.rb +121 -0
  162. data/lib/ea/qea/validation/database/circular_reference_validator.rb +109 -0
  163. data/lib/ea/qea/validation/database/orphan_validator.rb +153 -0
  164. data/lib/ea/qea/validation/database/referential_integrity_validator.rb +128 -0
  165. data/lib/ea/qea/validation/database.rb +16 -0
  166. data/lib/ea/qea/validation/diagram_validator.rb +112 -0
  167. data/lib/ea/qea/validation/formatters/json_formatter.rb +137 -0
  168. data/lib/ea/qea/validation/formatters/text_formatter.rb +235 -0
  169. data/lib/ea/qea/validation/formatters.rb +12 -0
  170. data/lib/ea/qea/validation/operation_validator.rb +71 -0
  171. data/lib/ea/qea/validation/package_validator.rb +111 -0
  172. data/lib/ea/qea/validation/validation_engine.rb +387 -0
  173. data/lib/ea/qea/validation/validation_message.rb +144 -0
  174. data/lib/ea/qea/validation/validation_result.rb +210 -0
  175. data/lib/ea/qea/validation/validator_registry.rb +134 -0
  176. data/lib/ea/qea/validation.rb +28 -0
  177. data/lib/ea/qea/verification/comparison_result.rb +264 -0
  178. data/lib/ea/qea/verification/document_normalizer.rb +169 -0
  179. data/lib/ea/qea/verification/document_verifier.rb +322 -0
  180. data/lib/ea/qea/verification/element_comparator.rb +277 -0
  181. data/lib/ea/qea/verification/structure_matcher.rb +287 -0
  182. data/lib/ea/qea/verification.rb +14 -0
  183. data/lib/ea/qea.rb +185 -0
  184. data/lib/ea/transformations/configuration.rb +333 -0
  185. data/lib/ea/transformations/format_registry.rb +366 -0
  186. data/lib/ea/transformations/parsers/base_parser.rb +482 -0
  187. data/lib/ea/transformations/parsers/qea_parser.rb +401 -0
  188. data/lib/ea/transformations/parsers/xmi_parser.rb +243 -0
  189. data/lib/ea/transformations/transformation_engine.rb +390 -0
  190. data/lib/ea/transformations.rb +85 -0
  191. data/lib/ea/transformers/qea_to_xmi/association_end.rb +19 -0
  192. data/lib/ea/transformers/qea_to_xmi/cardinality.rb +96 -0
  193. data/lib/ea/transformers/qea_to_xmi/context.rb +106 -0
  194. data/lib/ea/transformers/qea_to_xmi/guid_format.rb +56 -0
  195. data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +92 -0
  196. data/lib/ea/transformers/qea_to_xmi/run_state.rb +107 -0
  197. data/lib/ea/transformers/qea_to_xmi/transformer.rb +607 -0
  198. data/lib/ea/transformers/qea_to_xmi/visibility.rb +73 -0
  199. data/lib/ea/transformers/qea_to_xmi.rb +29 -0
  200. data/lib/ea/transformers/uml_to_xmi/id_generator.rb +54 -0
  201. data/lib/ea/transformers/uml_to_xmi/transformer.rb +152 -0
  202. data/lib/ea/transformers/uml_to_xmi/writer.rb +96 -0
  203. data/lib/ea/transformers/uml_to_xmi.rb +16 -0
  204. data/lib/ea/transformers.rb +34 -0
  205. data/lib/ea/version.rb +1 -1
  206. data/lib/ea/xmi/liquid_drops/association_drop.rb +56 -0
  207. data/lib/ea/xmi/liquid_drops/attribute_drop.rb +72 -0
  208. data/lib/ea/xmi/liquid_drops/cardinality_drop.rb +35 -0
  209. data/lib/ea/xmi/liquid_drops/connector_drop.rb +54 -0
  210. data/lib/ea/xmi/liquid_drops/constraint_drop.rb +29 -0
  211. data/lib/ea/xmi/liquid_drops/data_type_drop.rb +63 -0
  212. data/lib/ea/xmi/liquid_drops/dependency_drop.rb +36 -0
  213. data/lib/ea/xmi/liquid_drops/diagram_drop.rb +34 -0
  214. data/lib/ea/xmi/liquid_drops/enum_drop.rb +49 -0
  215. data/lib/ea/xmi/liquid_drops/enum_owned_literal_drop.rb +25 -0
  216. data/lib/ea/xmi/liquid_drops/generalization_attribute_drop.rb +87 -0
  217. data/lib/ea/xmi/liquid_drops/generalization_drop.rb +127 -0
  218. data/lib/ea/xmi/liquid_drops/klass_drop.rb +191 -0
  219. data/lib/ea/xmi/liquid_drops/operation_drop.rb +29 -0
  220. data/lib/ea/xmi/liquid_drops/package_drop.rb +108 -0
  221. data/lib/ea/xmi/liquid_drops/root_drop.rb +34 -0
  222. data/lib/ea/xmi/liquid_drops/source_target_drop.rb +43 -0
  223. data/lib/ea/xmi/lookup_service.rb +89 -0
  224. data/lib/ea/xmi/parser.rb +919 -0
  225. data/lib/ea/xmi.rb +35 -0
  226. data/lib/ea.rb +10 -1
  227. metadata +382 -9
@@ -0,0 +1,283 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Qea
5
+ module Factory
6
+ # Builds and validates UML Document structure
7
+ # Ensures all required document sections are populated correctly
8
+ class DocumentBuilder
9
+ attr_reader :document
10
+
11
+ # Initialize builder with new document
12
+ # @param name [String] Document name
13
+ def initialize(name: "EA Model")
14
+ @document = Lutaml::Uml::Document.new
15
+ @document.name = name
16
+ # Don't initialize collections - they have default values
17
+ end
18
+
19
+ # Add packages to document
20
+ # @param packages [Array<Lutaml::Uml::Package>] Packages to add
21
+ # @return [self] For method chaining
22
+ def add_packages(packages)
23
+ return self if packages.nil? || packages.empty?
24
+
25
+ @document.packages.concat(packages)
26
+ self
27
+ end
28
+
29
+ # Add classes to document
30
+ # @param classes [Array<Lutaml::Uml::UmlClass>] Classes to add
31
+ # @return [self] For method chaining
32
+ def add_classes(classes)
33
+ return self if classes.nil? || classes.empty?
34
+
35
+ @document.classes.concat(classes)
36
+ self
37
+ end
38
+
39
+ # Add enums to document
40
+ # @param enums [Array<Lutaml::Uml::Enum>] Enums to add
41
+ # @return [self] For method chaining
42
+ def add_enums(enums)
43
+ return self if enums.nil? || enums.empty?
44
+
45
+ @document.enums.concat(enums)
46
+ self
47
+ end
48
+
49
+ # Add data types to document
50
+ # @param data_types [Array<Lutaml::Uml::DataType>] Data types to add
51
+ # @return [self] For method chaining
52
+ def add_data_types(data_types)
53
+ return self if data_types.nil? || data_types.empty?
54
+
55
+ @document.data_types.concat(data_types)
56
+ self
57
+ end
58
+
59
+ # Add instances to document
60
+ # @param instances [Array<Lutaml::Uml::Instance>] Instances to add
61
+ # @return [self] For method chaining
62
+ def add_instances(instances)
63
+ return self if instances.nil? || instances.empty?
64
+
65
+ @document.instances.concat(instances)
66
+ self
67
+ end
68
+
69
+ # Add associations to document
70
+ # @param associations [Array<Lutaml::Uml::Association>] Associations
71
+ # @return [self] For method chaining
72
+ def add_associations(associations)
73
+ return self if associations.nil? || associations.empty?
74
+
75
+ @document.associations.concat(associations)
76
+ self
77
+ end
78
+
79
+ # Add diagrams to document
80
+ # @param diagrams [Array<Lutaml::Uml::Diagram>] Diagrams
81
+ # @return [self] For method chaining
82
+ def add_diagrams(diagrams)
83
+ return self if diagrams.nil? || diagrams.empty?
84
+
85
+ @document.diagrams.concat(diagrams)
86
+ self
87
+ end
88
+
89
+ # Set document metadata
90
+ # @param title [String] Document title
91
+ # @param caption [String] Document caption
92
+ # @return [self] For method chaining
93
+ def set_metadata(title: nil, caption: nil)
94
+ @document.title = title if title
95
+ @document.caption = caption if caption
96
+ self
97
+ end
98
+
99
+ # Build and return the document
100
+ # @param validate [Boolean] Whether to validate before returning
101
+ # @return [Lutaml::Uml::Document] The built document
102
+ # @raise [ValidationError] If validation fails
103
+ def build(validate: true)
104
+ validate! if validate
105
+ @document
106
+ end
107
+
108
+ # Validate document integrity
109
+ # @return [Boolean] True if valid
110
+ # @raise [ValidationError] If validation fails
111
+ def validate! # rubocop:disable Metrics/MethodLength
112
+ errors = []
113
+ warnings = []
114
+
115
+ # Check for duplicate xmi_ids
116
+ check_duplicate_xmi_ids(errors)
117
+
118
+ # Check association references (warnings only for missing refs)
119
+ check_association_references(warnings)
120
+
121
+ # Print warnings if any
122
+ unless warnings.empty?
123
+ warn "Document validation warnings:"
124
+ warnings.each { |w| warn " - #{w}" }
125
+ end
126
+
127
+ raise ValidationError, errors.join("; ") unless errors.empty?
128
+
129
+ true
130
+ end
131
+
132
+ # Get document statistics
133
+ # @return [Hash] Statistics about document contents
134
+ def stats
135
+ {
136
+ packages: @document.packages.size,
137
+ classes: @document.classes.size,
138
+ enums: @document.enums.size,
139
+ data_types: @document.data_types.size,
140
+ instances: @document.instances.size,
141
+ associations: @document.associations.size,
142
+ }
143
+ end
144
+
145
+ private
146
+
147
+ # Check for duplicate xmi_ids across all elements
148
+ # @param errors [Array<String>] Error accumulator
149
+ def check_duplicate_xmi_ids(errors)
150
+ xmi_ids = collect_all_xmi_ids
151
+ duplicates = xmi_ids.group_by { |id| id }
152
+ .select { |_, v| v.size > 1 }
153
+ .keys
154
+
155
+ unless duplicates.empty?
156
+ errors << "Duplicate xmi_ids found: #{duplicates.join(', ')}"
157
+ end
158
+ end
159
+
160
+ # Collect all xmi_ids from document
161
+ # @return [Array<String>] All xmi_ids
162
+ def collect_all_xmi_ids # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
163
+ ids = []
164
+
165
+ # Collect from top-level elements
166
+ ids.concat(@document.packages.filter_map(&:xmi_id))
167
+ ids.concat(@document.classes.filter_map(&:xmi_id))
168
+ ids.concat(@document.enums.filter_map(&:xmi_id))
169
+ ids.concat(@document.data_types.filter_map(&:xmi_id))
170
+ ids.concat(@document.instances.filter_map(&:xmi_id))
171
+
172
+ # Recursively collect from packages (where most classes actually are)
173
+ @document.packages.each do |package|
174
+ ids.concat(collect_package_xmi_ids(package))
175
+ end
176
+
177
+ ids
178
+ end
179
+
180
+ # Recursively collect all xmi_ids from a package and its descendants
181
+ # @param package [Lutaml::Uml::Package] Package to collect from
182
+ # @return [Array<String>] All xmi_ids in package hierarchy
183
+ def collect_package_xmi_ids(package) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
184
+ ids = []
185
+
186
+ # Collect from package's elements
187
+ ids.concat(package.classes.filter_map(&:xmi_id)) if package.classes
188
+ ids.concat(package.enums.filter_map(&:xmi_id)) if package.enums
189
+ if package.data_types
190
+ ids.concat(package.data_types.filter_map(&:xmi_id))
191
+ end
192
+ if package.instances
193
+ ids.concat(package.instances.filter_map(&:xmi_id))
194
+ end
195
+
196
+ # Recursively collect from child packages
197
+ package.packages&.each do |child_package|
198
+ ids.concat(collect_package_xmi_ids(child_package))
199
+ end
200
+
201
+ ids
202
+ end
203
+
204
+ # Check that all association references are valid
205
+ # @param warnings [Array<String>] Warning accumulator
206
+ def check_association_references(warnings) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
207
+ return if @document.associations.empty?
208
+
209
+ all_xmi_ids = collect_all_xmi_ids.to_set
210
+ invalid_associations = []
211
+
212
+ @document.associations.each do |assoc|
213
+ has_invalid_member = !check_association_end_valid?(
214
+ assoc, :member_end_xmi_id, all_xmi_ids
215
+ )
216
+ has_invalid_owner = !check_association_end_valid?(
217
+ assoc, :owner_end_xmi_id, all_xmi_ids
218
+ )
219
+
220
+ if has_invalid_member || has_invalid_owner
221
+ invalid_associations << assoc
222
+ if has_invalid_member
223
+ add_invalid_end_warning(assoc, :member_end_xmi_id, all_xmi_ids,
224
+ warnings)
225
+ end
226
+ if has_invalid_owner
227
+ add_invalid_end_warning(assoc, :owner_end_xmi_id, all_xmi_ids,
228
+ warnings)
229
+ end
230
+ end
231
+ end
232
+
233
+ # Remove invalid associations from document
234
+ unless invalid_associations.empty?
235
+ @document.associations.reject! do |a|
236
+ invalid_associations.include?(a)
237
+ end
238
+ warnings << "Removed #{invalid_associations.size} association(s) " \
239
+ "with invalid references"
240
+ end
241
+ end
242
+
243
+ # Check if association end reference is valid
244
+ # @param assoc [Lutaml::Uml::Association] Association
245
+ # @param attr [Symbol] Attribute to check (should be xmi_id attribute)
246
+ # @param valid_ids [Set<String>] Set of valid xmi_ids
247
+ # @return [Boolean] True if valid or nil
248
+ def check_association_end_valid?(assoc, attr, valid_ids)
249
+ value = assoc.public_send(attr)
250
+ return true if value.nil?
251
+
252
+ valid_ids.include?(value)
253
+ end
254
+
255
+ # Add warning for invalid association end
256
+ # @param assoc [Lutaml::Uml::Association] Association
257
+ # @param attr [Symbol] Attribute to check (should be xmi_id attribute)
258
+ # @param valid_ids [Set<String>] Set of valid xmi_ids
259
+ # @param warnings [Array<String>] Warning accumulator
260
+ def add_invalid_end_warning(assoc, attr, valid_ids, warnings) # rubocop:disable Metrics/MethodLength
261
+ value = assoc.public_send(attr)
262
+ return if value.nil?
263
+
264
+ unless valid_ids.include?(value)
265
+ # Get the corresponding name attribute for better error messages
266
+ name_attr = attr.to_s.gsub("_xmi_id", "").to_sym
267
+ name_value = begin
268
+ assoc.public_send(name_attr)
269
+ rescue StandardError
270
+ nil
271
+ end
272
+
273
+ warnings << "Association #{assoc.xmi_id} references " \
274
+ "invalid #{name_attr}: #{name_value}"
275
+ end
276
+ end
277
+
278
+ # Custom validation error
279
+ class ValidationError < Ea::Error; end
280
+ end
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Qea
5
+ module Factory
6
+ # Main factory for orchestrating EA to UML transformation
7
+ # Implements Facade pattern for complete model transformation
8
+ class EaToUmlFactory
9
+ attr_reader :database, :options, :resolver
10
+
11
+ # Initialize factory with EA database
12
+ # @param database [Ea::Qea::Database] Loaded EA database
13
+ # @param options [Hash] Transformation options
14
+ # @option options [Boolean] :include_diagrams Include diagrams
15
+ # (default: true)
16
+ # @option options [Boolean] :validate Validate output (default: true)
17
+ # @option options [String] :document_name Document name
18
+ def initialize(database, options = {})
19
+ @database = database
20
+ @options = default_options.merge(options)
21
+ @resolver = ReferenceResolver.new
22
+ @transformers = {}
23
+ end
24
+
25
+ # Create complete UML document from EA database
26
+ # @return [Lutaml::Uml::Document] Complete UML document
27
+ def create_document # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
28
+ builder = DocumentBuilder.new(
29
+ name: options[:document_name] || "EA Model",
30
+ )
31
+
32
+ # Transform packages with hierarchy (includes all classes)
33
+ packages = transform_packages
34
+
35
+ # Transform associations (references classes by xmi_id)
36
+ associations = transform_associations
37
+
38
+ # Collect class-level associations from packages
39
+ # class-level associations contain associations with both directions
40
+ # and it may include associations in connector level
41
+ # i.e. owner_end -> member_end and member_end -> owner_end
42
+ class_associations = collect_class_association(packages)
43
+
44
+ # Orphaned classes: EA class objects whose package_id does not
45
+ # resolve to a known package. Attach them to the document root so
46
+ # their associations can resolve.
47
+ orphan_classes = transform_orphan_classes
48
+
49
+ # Build document with both connector-level and
50
+ # class-level associations
51
+ builder.add_packages(packages)
52
+ .add_classes(orphan_classes)
53
+ .add_associations(associations)
54
+ .add_associations(class_associations)
55
+
56
+ # Add diagrams if requested
57
+ if options[:include_diagrams]
58
+ builder.add_diagrams(transform_diagrams)
59
+ end
60
+
61
+ builder.build(validate: options[:validate])
62
+ end
63
+
64
+ # Transform all packages with hierarchy
65
+ # @return [Array<Lutaml::Uml::Package>] Root packages
66
+ def transform_packages # rubocop:disable Metrics/MethodLength
67
+ root_packages = database.packages.select do |pkg|
68
+ pkg.parent_id.nil? || pkg.parent_id.zero?
69
+ end
70
+
71
+ package_transformer = get_transformer(:package)
72
+ root_packages.filter_map do |ea_package|
73
+ uml_package = package_transformer.transform_with_hierarchy(
74
+ ea_package,
75
+ include_children: true,
76
+ )
77
+
78
+ register_package_hierarchy(uml_package)
79
+
80
+ uml_package
81
+ end
82
+ end
83
+
84
+ # Transform EA class objects that have no resolvable parent package.
85
+ #
86
+ # EA occasionally stores class rows whose `package_id` does not
87
+ # correspond to any row in `t_package` (typically the result of a
88
+ # deleted package whose children were not reparented). These classes
89
+ # would otherwise be silently dropped, breaking any associations that
90
+ # reference them. Attach them to the document root instead.
91
+ #
92
+ # @return [Array<Lutaml::Uml::UmlClass>] Orphaned UML classes
93
+ def transform_orphan_classes
94
+ class_transformer = get_transformer(:class)
95
+
96
+ orphans = ea_class_objects.reject do |ea_object|
97
+ package_known?(ea_object.package_id)
98
+ end
99
+
100
+ uml_classes = class_transformer.transform_collection(orphans)
101
+ uml_classes.each { |uml_class| register_element(uml_class) }
102
+ uml_classes
103
+ end
104
+
105
+ # All EA class-like objects (classes and interfaces)
106
+ # @return [Array<Ea::Qea::Models::EaObject>]
107
+ def ea_class_objects
108
+ database.objects.find_by_type("Class") +
109
+ database.objects.find_by_type("Interface")
110
+ end
111
+
112
+ # Whether a package_id resolves to a known package in t_package
113
+ # @param package_id [Integer, nil]
114
+ # @return [Boolean]
115
+ def package_known?(package_id)
116
+ return false if package_id.nil?
117
+
118
+ !database.find_package(package_id).nil?
119
+ end
120
+
121
+ # Transform all associations
122
+ # @return [Array<Lutaml::Uml::Association>] All UML associations
123
+ def transform_associations # rubocop:disable Metrics/MethodLength
124
+ association_transformer = get_transformer(:association)
125
+
126
+ ea_associations = database.connectors.select(&:association?)
127
+
128
+ uml_associations = association_transformer.transform_collection(
129
+ ea_associations,
130
+ )
131
+
132
+ uml_associations.each do |uml_assoc|
133
+ register_element(uml_assoc)
134
+ end
135
+
136
+ uml_associations
137
+ end
138
+
139
+ # Transform all diagrams
140
+ # @return [Array<Lutaml::Uml::Diagram>] All UML diagrams
141
+ def transform_diagrams
142
+ diagram_transformer = get_transformer(:diagram)
143
+ diagram_transformer.transform_collection(database.diagrams)
144
+ end
145
+
146
+ # Register custom transformers, overriding registry defaults
147
+ # @param transformers [Hash] Custom transformer instances keyed by type
148
+ # @return [self] For method chaining
149
+ def with_transformers(transformers)
150
+ @transformers.merge!(transformers)
151
+ self
152
+ end
153
+
154
+ private
155
+
156
+ def default_options
157
+ {
158
+ include_diagrams: true,
159
+ validate: true,
160
+ document_name: "EA Model",
161
+ }
162
+ end
163
+
164
+ # Get or create transformer by type using the TransformerRegistry
165
+ # @param type [Symbol] Transformer type
166
+ # @return [BaseTransformer] Transformer instance
167
+ def get_transformer(type)
168
+ return @transformers[type] if @transformers.key?(type)
169
+
170
+ transformer_class = TransformerRegistry.transformer_for(type)
171
+ unless transformer_class
172
+ raise ArgumentError, "Unknown transformer type: #{type}"
173
+ end
174
+
175
+ @transformers[type] = transformer_class.new(database)
176
+ end
177
+
178
+ def register_package_hierarchy(package)
179
+ return if package.nil?
180
+
181
+ register_element(package)
182
+ register_package_members(package)
183
+
184
+ package.packages&.each do |child_package|
185
+ register_package_hierarchy(child_package)
186
+ end
187
+ end
188
+
189
+ MEMBER_COLLECTIONS = %i[classes enums data_types instances].freeze
190
+
191
+ def register_package_members(package)
192
+ MEMBER_COLLECTIONS.each do |collection|
193
+ package.public_send(collection)&.each do |elem|
194
+ register_element(elem)
195
+ end
196
+ end
197
+ end
198
+
199
+ def register_element(element)
200
+ return if element.nil? || element.xmi_id.nil?
201
+
202
+ @resolver.register(element.xmi_id, element)
203
+ end
204
+
205
+ def collect_class_association(packages)
206
+ associations = []
207
+
208
+ packages.each do |package|
209
+ collect_package_associations(package, associations)
210
+ end
211
+
212
+ associations
213
+ end
214
+
215
+ def collect_package_associations(package, associations) # rubocop:disable Metrics/CyclomaticComplexity
216
+ package.classes&.each do |klass|
217
+ if klass.associations && !klass.associations.empty?
218
+ associations.concat(klass.associations)
219
+ end
220
+ end
221
+
222
+ package.packages&.each do |child_package|
223
+ collect_package_associations(child_package, associations)
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Qea
5
+ module Factory
6
+ # Transforms EA objects (Enumeration type) to UML enums
7
+ class EnumTransformer < BaseTransformer
8
+ # Transform EA object to UML enum
9
+ # @param ea_object [EaObject] EA object model
10
+ # @return [Lutaml::Uml::Enum] UML enum
11
+ def transform(ea_object)
12
+ return nil if ea_object.nil?
13
+ return nil unless enum?(ea_object)
14
+
15
+ Lutaml::Uml::Enum.new.tap do |enum|
16
+ assign_enum_basic(enum, ea_object)
17
+ assign_enum_features(enum, ea_object)
18
+ end
19
+ end
20
+
21
+ def assign_enum_basic(enum, ea_object)
22
+ enum.name = ea_object.name
23
+ enum.xmi_id = normalize_guid_to_xmi_format(ea_object.ea_guid,
24
+ "EAID")
25
+ enum.visibility = map_visibility(ea_object.visibility)
26
+ enum.stereotype = [ea_object.stereotype] if valid_stereotype?(ea_object)
27
+ enum.definition = ea_object.note if note_present?(ea_object)
28
+ end
29
+
30
+ def note_present?(ea_object)
31
+ ea_object.note && !ea_object.note.empty?
32
+ end
33
+
34
+ def assign_enum_features(enum, ea_object)
35
+ enum.values = load_enum_values(ea_object.ea_object_id)
36
+ enum.tagged_values = load_tagged_values(ea_object.ea_guid)
37
+ end
38
+
39
+ private
40
+
41
+ def enum?(ea_object)
42
+ ea_object.enumeration? ||
43
+ (ea_object.stereotype && ea_object.stereotype.downcase == "enumeration")
44
+ end
45
+
46
+ def valid_stereotype?(ea_object)
47
+ ea_object.stereotype && !ea_object.stereotype.empty?
48
+ end
49
+
50
+ # Load enum values (literals) from attributes
51
+ # @param object_id [Integer] Object ID
52
+ # @return [Array<Lutaml::Uml::Value>] Enum values
53
+ def load_enum_values(object_id)
54
+ return [] if object_id.nil?
55
+
56
+ ea_attrs = database.attributes_for_object(object_id)
57
+ .sort_by { |a| a.pos || 0 }
58
+
59
+ ea_attrs.filter_map { |ea_attr| build_enum_value(ea_attr) }
60
+ end
61
+
62
+ def build_enum_value(ea_attr)
63
+ Lutaml::Uml::Value.new.tap do |value|
64
+ value.name = ea_attr.name
65
+ value.id = normalize_guid_to_xmi_format(ea_attr.ea_guid, "EAID")
66
+ value.definition = ea_attr.notes unless
67
+ ea_attr.notes.nil? || ea_attr.notes.empty?
68
+ end
69
+ end
70
+
71
+ end
72
+ end
73
+ end
74
+ end