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,390 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Transformations
5
+ # Transformation Engine orchestrates the entire model transformation
6
+ # process.
7
+ #
8
+ # This class implements the Facade pattern to provide a simple interface
9
+ # for complex model transformation operations. It coordinates between
10
+ # configuration, format detection, parser selection, and transformation.
11
+ #
12
+ # The engine follows the Dependency Inversion Principle by depending on
13
+ # abstractions (BaseParser interface) rather than concrete implementations.
14
+ #
15
+ # @example Basic usage
16
+ # engine = TransformationEngine.new
17
+ # document = engine.parse("model.xmi")
18
+ #
19
+ # @example With custom configuration
20
+ # config = Configuration.load("my_config.yml")
21
+ # engine = TransformationEngine.new(config)
22
+ # document = engine.parse("model.qea")
23
+ class TransformationEngine
24
+ # @return [Configuration] Current configuration
25
+ attr_reader :configuration
26
+
27
+ # @return [FormatRegistry] Format registry
28
+ attr_reader :format_registry
29
+
30
+ # @return [Array<Hash>] Transformation history
31
+ attr_reader :transformation_history
32
+
33
+ # @return [Parser] Parser instance
34
+ attr_reader :current_parser
35
+
36
+ # Initialize transformation engine
37
+ #
38
+ # @param configuration [Configuration, nil] Configuration to use
39
+ # (defaults to auto-loaded configuration)
40
+ def initialize(configuration = nil)
41
+ @configuration = configuration || Configuration.load
42
+ @format_registry = FormatRegistry.new
43
+ @transformation_history = []
44
+ @parser_cache = {}
45
+
46
+ # Load parsers from configuration
47
+ setup_parsers
48
+ end
49
+
50
+ # Parse a model file into a UML document
51
+ #
52
+ # This is the main entry point for model transformation. It auto-detects
53
+ # the file format and uses the appropriate parser.
54
+ #
55
+ # @param file_path [String] Path to the model file
56
+ # @param options [Hash] Parsing options (merged with configuration)
57
+ # @return [Lutaml::Uml::Document] Parsed UML document
58
+ # @raise [UnsupportedFormatError] if file format is not supported
59
+ # @raise [ParseError] if parsing fails
60
+ def parse(file_path, options = {}) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
61
+ validate_file_path!(file_path)
62
+
63
+ # Detect format and get parser
64
+ parser_class = detect_parser(file_path)
65
+ raise UnsupportedFormatError.new(file_path) unless parser_class
66
+
67
+ # Create parser instance with merged options
68
+ merged_options = merge_options(options)
69
+ @current_parser = get_parser_instance(parser_class, merged_options)
70
+
71
+ # Record transformation start
72
+ transformation_start = Time.now
73
+
74
+ begin
75
+ # Perform parsing
76
+ document = @current_parser.parse(file_path)
77
+
78
+ # Record successful transformation
79
+ record_transformation(
80
+ file_path: file_path,
81
+ parser: @current_parser,
82
+ duration: Time.now - transformation_start,
83
+ success: true,
84
+ document: document,
85
+ )
86
+
87
+ document
88
+ rescue StandardError => e
89
+ # Record failed transformation
90
+ record_transformation(
91
+ file_path: file_path,
92
+ parser: @current_parser,
93
+ duration: Time.now - transformation_start,
94
+ success: false,
95
+ error: e,
96
+ )
97
+
98
+ # Re-raise the error
99
+ raise
100
+ end
101
+ end
102
+
103
+ # Auto-detect file format and return appropriate parser class
104
+ #
105
+ # Uses multiple detection strategies:
106
+ # 1. File extension
107
+ # 2. Content detection (magic bytes)
108
+ # 3. Fallback parser from configuration
109
+ #
110
+ # @param file_path [String] Path to the model file
111
+ # @return [Class, nil] Parser class, or nil if format cannot be detected
112
+ def detect_parser(file_path) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
113
+ # Strategy 1: File extension detection
114
+ if @configuration.file_extension_detection_enabled?
115
+ parser_class = @format_registry.parser_for_file(file_path)
116
+ return parser_class if parser_class
117
+ end
118
+
119
+ # Strategy 2: Content detection
120
+ if @configuration.content_sniffing_enabled?
121
+ parser_class = @format_registry.detect_by_content(file_path)
122
+ return parser_class if parser_class
123
+ end
124
+
125
+ # Strategy 3: Fallback parser
126
+ fallback_parser_name = @configuration.fallback_parser
127
+ if fallback_parser_name
128
+ return Transformations.constantize(fallback_parser_name)
129
+ end
130
+
131
+ nil
132
+ rescue StandardError
133
+ nil
134
+ end
135
+
136
+ # Get list of supported file extensions
137
+ #
138
+ # @return [Array<String>] List of supported extensions
139
+ def supported_extensions
140
+ @format_registry.supported_extensions
141
+ end
142
+
143
+ # Check if a file format is supported
144
+ #
145
+ # @param file_path [String] Path to check
146
+ # @return [Boolean] true if format is supported
147
+ def supports_file?(file_path)
148
+ detect_parser(file_path) != nil
149
+ end
150
+
151
+ # Register a custom parser for a file extension
152
+ #
153
+ # @param extension [String] File extension (e.g., ".custom")
154
+ # @param parser_class [Class] Parser class implementing BaseParser
155
+ # interface
156
+ # @return [void]
157
+ def register_parser(extension, parser_class)
158
+ @format_registry.register(extension, parser_class)
159
+ end
160
+
161
+ # Unregister a parser for a file extension
162
+ #
163
+ # @param extension [String] File extension to unregister
164
+ # @return [Class, nil] The unregistered parser class
165
+ def unregister_parser(extension)
166
+ @format_registry.unregister(extension)
167
+ end
168
+
169
+ # Set configuration and reload parsers
170
+ #
171
+ # @param config [Configuration] New configuration
172
+ # @return [void]
173
+ def configuration=(config)
174
+ @configuration = config
175
+ @parser_cache.clear
176
+ setup_parsers
177
+ end
178
+
179
+ # Get comprehensive transformation statistics
180
+ #
181
+ # @return [Hash] Statistics about transformations
182
+ def statistics # rubocop:disable Metrics/MethodLength
183
+ successful_transformations = @transformation_history.count do |t|
184
+ t[:success]
185
+ end
186
+ failed_transformations = @transformation_history.count do |t|
187
+ !t[:success]
188
+ end
189
+
190
+ {
191
+ total_transformations: @transformation_history.size,
192
+ successful_transformations: successful_transformations,
193
+ failed_transformations: failed_transformations,
194
+ success_rate: calculate_success_rate,
195
+ average_duration: calculate_average_duration,
196
+ supported_extensions: supported_extensions,
197
+ registered_parsers: @format_registry.all_parsers.keys,
198
+ configuration_version: @configuration.version,
199
+ }
200
+ end
201
+
202
+ # Clear transformation history
203
+ #
204
+ # @return [void]
205
+ def clear_history
206
+ @transformation_history.clear
207
+ end
208
+
209
+ # Get transformation history for a specific file
210
+ #
211
+ # @param file_path [String] Path to the file
212
+ # @return [Array<Hash>] Transformation history entries for the file
213
+ def history_for_file(file_path)
214
+ @transformation_history.select do |entry|
215
+ entry[:file_path] == file_path
216
+ end
217
+ end
218
+
219
+ # Get recent transformation failures
220
+ #
221
+ # @param limit [Integer] Maximum number of failures to return
222
+ # @return [Array<Hash>] Recent failure entries
223
+ def recent_failures(limit = 10)
224
+ @transformation_history
225
+ .reject { |entry| entry[:success] }
226
+ .last(limit)
227
+ end
228
+
229
+ # Validate configuration and parsers
230
+ #
231
+ # @return [Hash] Validation results
232
+ def validate_setup # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
233
+ results = {
234
+ configuration_valid: false,
235
+ parsers_loaded: 0,
236
+ parser_errors: [],
237
+ warnings: [],
238
+ }
239
+
240
+ # Validate configuration
241
+ begin
242
+ if @configuration&.enabled_parsers&.any?
243
+ results[:configuration_valid] = true
244
+ else
245
+ results[:warnings] << "No enabled parsers in configuration"
246
+ end
247
+ rescue StandardError => e
248
+ results[:parser_errors] << "Configuration error: #{e.message}"
249
+ end
250
+
251
+ # Validate each parser
252
+ @format_registry.all_parsers.each_value do |parser_class|
253
+ # Try to create instance to validate
254
+ parser = parser_class.new(configuration: @configuration)
255
+ if parser.is_a?(Parsers::BaseParser)
256
+ results[:parsers_loaded] += 1
257
+ else
258
+ results[:parser_errors] << "Parser #{parser_class} does not " \
259
+ "implement parse method"
260
+ end
261
+ rescue StandardError => e
262
+ results[:parser_errors] << "Failed to instantiate #{parser_class}: " \
263
+ "#{e.message}"
264
+ end
265
+
266
+ results
267
+ end
268
+
269
+ private
270
+
271
+ # Setup parsers from configuration
272
+ #
273
+ # @return [void]
274
+ def setup_parsers
275
+ # Clear existing parsers
276
+ @format_registry.clear
277
+
278
+ # Load parsers from configuration
279
+ @format_registry.load_from_configuration(@configuration)
280
+
281
+ # Load default parsers if none configured
282
+ if @format_registry.supported_extensions.empty?
283
+ @format_registry.load_default_parsers
284
+ end
285
+ end
286
+
287
+ # Get parser instance (with caching)
288
+ #
289
+ # @param parser_class [Class] Parser class
290
+ # @param options [Hash] Parser options
291
+ # @return [BaseParser] Parser instance
292
+ def get_parser_instance(parser_class, options)
293
+ cache_key = [parser_class, options.hash]
294
+
295
+ @parser_cache[cache_key] ||= parser_class.new(
296
+ configuration: @configuration,
297
+ options: options,
298
+ )
299
+ end
300
+
301
+ # Merge options with configuration defaults
302
+ #
303
+ # @param options [Hash] User-provided options
304
+ # @return [Hash] Merged options
305
+ def merge_options(options) # rubocop:disable Metrics/MethodLength
306
+ default_options = {}
307
+
308
+ if @configuration.transformation_options
309
+ default_options = {
310
+ validate_output: @configuration
311
+ .transformation_options.validate_output,
312
+ include_diagrams: @configuration
313
+ .transformation_options.include_diagrams,
314
+ preserve_ids: @configuration.transformation_options.preserve_ids,
315
+ resolve_references: @configuration
316
+ .transformation_options.resolve_references,
317
+ strict_mode: @configuration.transformation_options.strict_mode,
318
+ }
319
+ end
320
+
321
+ default_options.merge(options)
322
+ end
323
+
324
+ # Record transformation in history
325
+ #
326
+ # @param entry [Hash] Transformation entry
327
+ # @return [void]
328
+ def record_transformation(entry)
329
+ # Add timestamp and additional metadata
330
+ full_entry = entry.merge(
331
+ timestamp: Time.now,
332
+ engine_version: self.class.name,
333
+ configuration_version: @configuration.version,
334
+ )
335
+
336
+ @transformation_history.push(full_entry)
337
+ @transformation_history.shift if @transformation_history.size > 1000
338
+ end
339
+
340
+ # Calculate success rate
341
+ #
342
+ # @return [Float] Success rate as percentage
343
+ def calculate_success_rate
344
+ return 0.0 if @transformation_history.empty?
345
+
346
+ successful = @transformation_history.count { |t| t[:success] }
347
+ (successful.to_f / @transformation_history.size * 100).round(2)
348
+ end
349
+
350
+ # Calculate average transformation duration
351
+ #
352
+ # @return [Float] Average duration in seconds
353
+ def calculate_average_duration
354
+ return 0.0 if @transformation_history.empty?
355
+
356
+ total_duration = @transformation_history.sum { |t| t[:duration] || 0 }
357
+ (total_duration / @transformation_history.size).round(3)
358
+ end
359
+
360
+ # Validate file path
361
+ #
362
+ # @param file_path [String] File path to validate
363
+ # @raise [ArgumentError] if path is invalid
364
+ def validate_file_path!(file_path)
365
+ raise ArgumentError, "File path cannot be nil" if file_path.nil?
366
+ raise ArgumentError, "File path cannot be empty" if file_path.empty?
367
+
368
+ unless File.exist?(file_path)
369
+ raise ArgumentError,
370
+ "File does not exist: #{file_path}"
371
+ end
372
+ end
373
+ end
374
+
375
+ # Error class for unsupported file formats
376
+ class UnsupportedFormatError < Ea::Error
377
+ # @return [String] Path to the unsupported file
378
+ attr_reader :file_path
379
+
380
+ # Initialize error
381
+ #
382
+ # @param file_path [String] Path to unsupported file
383
+ def initialize(file_path)
384
+ @file_path = file_path
385
+ extension = File.extname(file_path)
386
+ super("Unsupported file format: #{extension} (file: #{file_path})")
387
+ end
388
+ end
389
+ end
390
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Transformations
5
+ autoload :Configuration, "ea/transformations/configuration"
6
+ autoload :FormatRegistry, "ea/transformations/format_registry"
7
+ autoload :TransformationEngine, "ea/transformations/transformation_engine"
8
+
9
+ module Parsers
10
+ autoload :BaseParser, "ea/transformations/parsers/base_parser"
11
+ autoload :XmiParser, "ea/transformations/parsers/xmi_parser"
12
+ autoload :QeaParser, "ea/transformations/parsers/qea_parser"
13
+ end
14
+
15
+ # Resolve a class name string to a constant
16
+ # @param class_name [String] Fully qualified class name (e.g. "Ea::Foo::Bar")
17
+ # @return [Class, nil] The resolved class constant, or nil if not found
18
+ def self.constantize(class_name)
19
+ parts = class_name.split("::")
20
+ constant = Object
21
+ parts.each { |part| constant = constant.const_get(part) }
22
+ constant
23
+ rescue NameError
24
+ nil
25
+ end
26
+
27
+ class << self
28
+ def engine
29
+ @engine ||= TransformationEngine.new
30
+ end
31
+
32
+ def engine=(engine)
33
+ @engine = engine
34
+ end
35
+
36
+ def parse(file_path, options = {})
37
+ engine.parse(file_path, options)
38
+ end
39
+
40
+ def detect_parser(file_path)
41
+ engine.detect_parser(file_path)
42
+ end
43
+
44
+ def supported_extensions
45
+ engine.supported_extensions
46
+ end
47
+
48
+ def supports_file?(file_path)
49
+ engine.supports_file?(file_path)
50
+ end
51
+
52
+ def statistics
53
+ engine.statistics
54
+ end
55
+
56
+ def reset_statistics
57
+ engine.clear_history
58
+ end
59
+
60
+ def validate_setup
61
+ engine.validate_setup
62
+ end
63
+
64
+ def register_parser(extension, parser_class)
65
+ engine.register_parser(extension, parser_class)
66
+ end
67
+
68
+ def load_configuration(config_path)
69
+ engine.configuration = Configuration.load(config_path)
70
+ end
71
+
72
+ def configuration
73
+ engine.configuration
74
+ end
75
+
76
+ def configuration=(config)
77
+ engine.configuration = config
78
+ end
79
+
80
+ def configure
81
+ yield(configuration) if block_given?
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Transformers
5
+ module QeaToXmi
6
+ # Lightweight value object returned by `Transformer#build_association_end`.
7
+ #
8
+ # Carries both the synthesised xmi:id of the `<ownedEnd>` element
9
+ # (used to populate `<memberEnd idref="...">` on the enclosing
10
+ # `<packagedElement xmi:type="uml:Association">`) and the model
11
+ # instance itself.
12
+ #
13
+ # Using a Struct (not a Hash) makes the contract visible at the
14
+ # call site: typos like `end.xmii_id` raise NoMethodError instead
15
+ # of silently returning nil.
16
+ AssociationEnd = Struct.new(:xmi_id, :model)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Transformers
5
+ module QeaToXmi
6
+ # Pure-function cardinality / multiplicity parser for EA's
7
+ # free-text bound fields (`t_attribute.upperbound`,
8
+ # `t_connector.sourcecard`, etc.).
9
+ #
10
+ # EA stores cardinality as opaque strings: `1`, `0..1`, `1..*`,
11
+ # `*`, occasionally `unbounded` or `*-1`. The XMI wire form needs
12
+ # two separate child elements (`<lowerValue value="N"/>` and
13
+ # `<upperValue value="M"/>`) — never a range string. This module
14
+ # translates the EA form to a `{ lower:, upper: }` pair.
15
+ #
16
+ # Default-when-empty: EA's "no bound specified" maps to UML's
17
+ # unspecified multiplicity, which Sparx renders as
18
+ # `<lowerValue value="0"/>` and `<upperValue value="-1"/>`.
19
+ # Always emitting both is required for round-trip parity with
20
+ # real Sparx XMI (see TODO 26).
21
+ module Cardinality
22
+ # Tokens EA uses for "unbounded". Matched case-insensitively.
23
+ UNLIMITED_TOKENS = %w[* *-1 unbounded].freeze
24
+
25
+ # UML defaults when EA carries no explicit bound.
26
+ DEFAULT_LOWER = "0"
27
+ DEFAULT_UPPER = "-1"
28
+
29
+ module_function
30
+
31
+ # @param raw [String, nil] e.g. "1..*", "0..1", "1", "*", nil
32
+ # @return [Hash{Symbol=>String}] `{ lower:, upper: }` always
33
+ # populated; never nil. Empty/nil input returns the UML default.
34
+ def parse(raw)
35
+ return defaults if raw.nil? || raw.to_s.strip.empty?
36
+
37
+ stripped = raw.to_s.strip
38
+ return parse_range(stripped) if stripped.include?("..")
39
+
40
+ # Bare unlimited token (e.g. "*") means "many" — lower bound
41
+ # is unspecified, which UML renders as 0..-1. Returning
42
+ # `{ lower: "-1", upper: "-1" }` here would be invalid:
43
+ # LiteralInteger cannot hold -1.
44
+ return defaults if UNLIMITED_TOKENS.include?(stripped.downcase)
45
+
46
+ single = normalize_bound(stripped)
47
+ { lower: single, upper: single }
48
+ end
49
+
50
+ # Normalise an upper-bound token: `*` / `unbounded` → `-1`
51
+ # (UML LiteralUnlimitedNatural wire form).
52
+ # @param raw [String, Integer, nil]
53
+ # @return [String]
54
+ def normalize_upper(raw)
55
+ return DEFAULT_UPPER if raw.nil?
56
+
57
+ stripped = raw.to_s.strip
58
+ return DEFAULT_UPPER if stripped.empty?
59
+
60
+ UNLIMITED_TOKENS.include?(stripped.downcase) ? "-1" : stripped
61
+ end
62
+
63
+ # Normalise a lower-bound token: empty/nil → "0" (UML default).
64
+ # @param raw [String, Integer, nil]
65
+ # @return [String]
66
+ def normalize_lower(raw)
67
+ return DEFAULT_LOWER if raw.nil?
68
+
69
+ stripped = raw.to_s.strip
70
+ stripped.empty? ? DEFAULT_LOWER : stripped
71
+ end
72
+
73
+ # ---- Internal helpers ----
74
+
75
+ def defaults
76
+ { lower: DEFAULT_LOWER, upper: DEFAULT_UPPER }
77
+ end
78
+
79
+ def parse_range(stripped)
80
+ lower, upper = stripped.split("..", 2)
81
+ { lower: normalize_bound(lower), upper: normalize_bound(upper) }
82
+ end
83
+
84
+ # A single bound token (one side of `..` or a bare scalar).
85
+ # Empty / `*` → UML unlimited (`-1`).
86
+ def normalize_bound(token)
87
+ return "-1" if token.nil?
88
+ return "-1" if token.strip.empty?
89
+
90
+ stripped = token.strip
91
+ UNLIMITED_TOKENS.include?(stripped.downcase) ? "-1" : stripped
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Transformers
5
+ module QeaToXmi
6
+ # Shared state passed across the walk.
7
+ #
8
+ # Wraps the {Ea::Qea::Database} and provides:
9
+ # - ID-derivation helpers (`xmi_id_for`, `end_xmi_id_for`) backed by
10
+ # {GuidFormat}
11
+ # - delegated database lookups (objects, packages, attributes, etc.)
12
+ #
13
+ # The {Ea::Qea::Database} already maintains its own lookup indexes
14
+ # (object-by-id, connectors-by-object, attributes-by-object, etc.).
15
+ # This class delegates to those rather than re-indexing — single source
16
+ # of truth lives on the database.
17
+ class Context
18
+ attr_reader :database, :id_allocator
19
+
20
+ def initialize(database:)
21
+ @database = database
22
+ @id_allocator = IdAllocator.new
23
+ end
24
+
25
+ # ---- ID helpers ---------------------------------------------------
26
+
27
+ # @param record [#ea_guid]
28
+ # @param prefix [String] "EAID" (default) or "EAPK" for top-level packages
29
+ # @return [String, nil]
30
+ def xmi_id_for(record, prefix: "EAID")
31
+ return nil unless record&.ea_guid
32
+
33
+ GuidFormat.ea_guid_to_xmi_id(record.ea_guid, prefix: prefix)
34
+ end
35
+
36
+ # @param connector_xmi_id [String]
37
+ # @param side [Symbol] :source or :destination
38
+ # @return [String]
39
+ def end_xmi_id_for(connector_xmi_id, side:)
40
+ GuidFormat.connector_end_xmi_id(connector_xmi_id, side: side)
41
+ end
42
+
43
+ # ---- Database lookups --------------------------------------------
44
+
45
+ # @param ea_guid [String]
46
+ # @return [Ea::Qea::Models::EaObject, nil]
47
+ def object_by_guid(ea_guid)
48
+ database.find_object_by_guid(ea_guid)
49
+ end
50
+
51
+ # @param object_id [Integer]
52
+ # @return [Ea::Qea::Models::EaObject, nil]
53
+ def object_by_id(object_id)
54
+ database.find_object(object_id)
55
+ end
56
+
57
+ # @param package_id [Integer]
58
+ # @return [Array<Ea::Qea::Models::EaPackage>]
59
+ def child_packages(package_id)
60
+ database.child_packages_for(package_id)
61
+ end
62
+
63
+ # @param package_id [Integer]
64
+ # @return [Array<Ea::Qea::Models::EaObject>]
65
+ def objects_in_package(package_id)
66
+ database.objects_in_package(package_id)
67
+ end
68
+
69
+ # @param object_id [Integer]
70
+ # @return [Array<Ea::Qea::Models::EaAttribute>]
71
+ def attributes_for(object_id)
72
+ database.attributes_for_object(object_id)
73
+ end
74
+
75
+ # @param object_id [Integer]
76
+ # @return [Array<Ea::Qea::Models::EaOperation>]
77
+ def operations_for(object_id)
78
+ database.operations_for_object(object_id)
79
+ end
80
+
81
+ # @param operation_id [Integer]
82
+ # @return [Array<Ea::Qea::Models::EaOperationParam>]
83
+ def params_for_operation(operation_id)
84
+ database.operation_params_for(operation_id)
85
+ end
86
+
87
+ # Connectors where the object is on either side.
88
+ # @param object_id [Integer]
89
+ # @return [Array<Ea::Qea::Models::EaConnector>]
90
+ def connectors_for(object_id)
91
+ database.connectors_for_object(object_id)
92
+ end
93
+
94
+ # Connectors where this object is the start (source) — used to decide
95
+ # which package owns a relationship connector.
96
+ # @param object_id [Integer]
97
+ # @return [Array<Ea::Qea::Models::EaConnector>]
98
+ def connectors_starting_at(object_id)
99
+ database.connectors_for_object(object_id).select do |conn|
100
+ conn.start_object_id == object_id
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end