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,366 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Transformations
5
+ # Format Registry manages parser registration and discovery.
6
+ #
7
+ # This class implements the Registry pattern to provide a centralized
8
+ # location for managing format parsers. It follows the Open/Closed Principle
9
+ # by allowing new parsers to be registered without modifying existing code.
10
+ #
11
+ # @example Register a parser
12
+ # registry = FormatRegistry.new
13
+ # registry.register(".custom", MyCustomParser)
14
+ #
15
+ # @example Get parser for extension
16
+ # parser_class = registry.parser_for_extension(".xmi")
17
+ # parser = parser_class.new
18
+ class FormatRegistry
19
+ # @return [Hash<String, Class>] Map of extensions to parser classes
20
+ attr_reader :parsers
21
+
22
+ def initialize
23
+ @parsers = {}
24
+ @extensions = []
25
+ @default_parsers_loaded = false
26
+ end
27
+
28
+ # Register a parser for a file extension
29
+ #
30
+ # @param extension [String] File extension (e.g., ".xmi", ".qea")
31
+ # @param parser_class [Class] Parser class implementing BaseParser
32
+ # interface
33
+ # @raise [ArgumentError] if extension or parser_class is invalid
34
+ def register(extension, parser_class) # rubocop:disable Metrics/MethodLength
35
+ if extension.is_a?(Array)
36
+ extension.each { |ext| register(ext, parser_class) }
37
+ return
38
+ end
39
+
40
+ validate_extension!(extension)
41
+ validate_parser_class!(parser_class)
42
+
43
+ normalized_ext = normalize_extension(extension)
44
+ @parsers[normalized_ext] = parser_class
45
+ unless @extensions.include?(normalized_ext)
46
+ @extensions << normalized_ext
47
+ end
48
+ end
49
+
50
+ # Unregister a parser for a file extension
51
+ #
52
+ # @param extension [String] File extension to unregister
53
+ # @return [Class, nil] The unregistered parser class, or nil if not found
54
+ def unregister(extension)
55
+ normalized_ext = normalize_extension(extension)
56
+ @extensions.delete(normalized_ext)
57
+ @parsers.delete(normalized_ext)
58
+ end
59
+
60
+ # Auto-register a parser class based on its supported extensions
61
+ #
62
+ # @param parser_class [Class] Parser class inherited from BaseParser
63
+ # @return [void]
64
+ def auto_register_from_parser(parser_class)
65
+ unless parser_class.is_a?(Class) &&
66
+ parser_class <= Ea::Transformations::Parsers::BaseParser
67
+ raise ArgumentError,
68
+ "#{parser_class} must inherit from " \
69
+ "Ea::Transformations::Parsers::BaseParser"
70
+ end
71
+
72
+ extensions = parser_class.new.supported_extensions
73
+ raise ArgumentError, "Extension cannot be nil or empty" if extensions.nil? || extensions.empty?
74
+
75
+ register(extensions, parser_class)
76
+ end
77
+
78
+ # Get parser class for a file extension
79
+ #
80
+ # @param extension [String] File extension (e.g., ".xmi", ".qea")
81
+ # @return [Class, nil] Parser class, or nil if not found
82
+ def parser_for_extension(extension)
83
+ # ensure_default_parsers_loaded!
84
+ normalized_ext = normalize_extension(extension)
85
+ @parsers[normalized_ext]
86
+ end
87
+
88
+ # Get parser class for a file path
89
+ #
90
+ # @param file_path [String] Path to the file
91
+ # @return [Class, nil] Parser class, or nil if not found
92
+ def parser_for_file(file_path)
93
+ extension = File.extname(file_path)
94
+ parser_for_extension(extension)
95
+ end
96
+
97
+ # Check if an extension is supported
98
+ #
99
+ # @param extension [String] File extension to check
100
+ # @return [Boolean] true if extension is supported
101
+ def supports_extension?(extension)
102
+ parser_for_extension(extension) != nil
103
+ end
104
+
105
+ # Check if a file is supported
106
+ #
107
+ # @param file_path [String] Path to the file
108
+ # @return [Boolean] true if file format is supported
109
+ def supports_file?(file_path)
110
+ parser_for_file(file_path) != nil
111
+ end
112
+
113
+ # Get all supported extensions
114
+ #
115
+ # @return [Array<String>] List of supported file extensions
116
+ def supported_extensions
117
+ # ensure_default_parsers_loaded!
118
+ @parsers.keys.sort
119
+ end
120
+
121
+ # Get all registered parsers
122
+ #
123
+ # @return [Hash<String, Class>] Map of extensions to parser classes
124
+ def all_parsers
125
+ # ensure_default_parsers_loaded!
126
+ @parsers.dup
127
+ end
128
+
129
+ # Get parsers sorted by priority (highest first)
130
+ #
131
+ # @return [Array<Array(String, Class)>] List of [extension, parser_class]
132
+ def parsers_by_priority
133
+ @parsers.sort_by do |_ext, parser_class|
134
+ parser_class.new.priority
135
+ end.reverse
136
+ end
137
+
138
+ # Get all registered extensions
139
+ #
140
+ # @return [Array<String>] List of registered extensions
141
+ def all_extensions
142
+ @extensions.dup
143
+ end
144
+
145
+ # Get statistics about registered parsers
146
+ #
147
+ # @return [Hash] Statistics hash
148
+ def statistics # rubocop:disable Metrics/MethodLength
149
+ parsers = @parsers.values.uniq
150
+ total_parsers = parsers.size
151
+ ext_size = @extensions.size
152
+
153
+ {
154
+ total_parsers: total_parsers,
155
+ total_extensions: ext_size,
156
+ extensions_per_parser: (ext_size.to_f / total_parsers).round(2),
157
+ parser_details: parsers.map do |parser_class|
158
+ {
159
+ parser: parser_class,
160
+ extensions: parser_class.new.supported_extensions,
161
+ priority: parser_class.new.priority,
162
+ format_name: parser_class.new.format_name,
163
+ }
164
+ end,
165
+ }
166
+ end
167
+
168
+ def export_configuration # rubocop:disable Metrics/MethodLength
169
+ {
170
+ exported_at: Time.now,
171
+ parsers: @parsers.map do |parser|
172
+ _ext, parser_class = parser
173
+
174
+ {
175
+ parser_class: parser_class,
176
+ extensions: parser_class.new.supported_extensions,
177
+ priority: parser_class.new.priority,
178
+ format: parser_class.new.format_name,
179
+ }
180
+ end,
181
+ }
182
+ end
183
+
184
+ # Clear all registered parsers
185
+ #
186
+ # @return [void]
187
+ def clear
188
+ @extensions.clear
189
+ @parsers.clear
190
+ @default_parsers_loaded = false
191
+ end
192
+
193
+ # Load parsers from configuration
194
+ #
195
+ # @param configuration [Configuration] Configuration with parser
196
+ # definitions
197
+ # @return [void]
198
+ def load_from_configuration(configuration)
199
+ configuration.enabled_parsers.each do |parser_config|
200
+ parser_class = constantize_parser_class(parser_config.parser_class)
201
+ register(parser_config.extension, parser_class)
202
+ rescue NameError => e
203
+ warn "Warning: Could not load parser " \
204
+ "#{parser_config.parser_class}: #{e.message}"
205
+ end
206
+ end
207
+
208
+ # Create a new instance with default parsers loaded
209
+ #
210
+ # @return [FormatRegistry] New registry instance
211
+ def self.with_defaults
212
+ registry = new
213
+ registry.load_default_parsers
214
+ registry
215
+ end
216
+
217
+ # Load default parsers (called automatically when needed)
218
+ #
219
+ # @return [void]
220
+ def load_default_parsers # rubocop:disable Metrics/MethodLength
221
+ return if @default_parsers_loaded
222
+
223
+ # Load XMI parser if available
224
+ begin
225
+ register(".xmi", Parsers::XmiParser)
226
+ rescue LoadError
227
+ # XMI parser not available, skip
228
+ end
229
+
230
+ # Load QEA parser if available
231
+ begin
232
+ register(".qea", Parsers::QeaParser)
233
+ rescue LoadError
234
+ # QEA parser not available, skip
235
+ end
236
+
237
+ @default_parsers_loaded = true
238
+ end
239
+
240
+ # Detect format by file content (magic bytes/signatures)
241
+ #
242
+ # @param file_path [String] Path to the file
243
+ # @return [Class, nil] Parser class based on content detection
244
+ def detect_by_content(file_path) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
245
+ # ensure_default_parsers_loaded!
246
+ unless File.exist?(file_path)
247
+ raise ArgumentError, "#{file_path} does not exist!"
248
+ end
249
+
250
+ # Read first few bytes to detect format
251
+ File.open(file_path, "rb") do |file|
252
+ header = file.read(1024) # Read first 1KB
253
+
254
+ return nil if header.nil? || header.empty?
255
+
256
+ # Check header match for content patterns
257
+ @parsers.each do |ext, parser_class|
258
+ parser_klass = parser_class.new
259
+ parser_klass.content_patterns.each do |pattern|
260
+ if header.match?(pattern)
261
+ return parser_for_extension(ext)
262
+ end
263
+ end
264
+ end
265
+
266
+ # Check for XML/XMI signatures
267
+ if header.include?("<?xml") && header.include?("xmi:")
268
+ ensure_default_parsers_loaded!
269
+ return parser_for_extension(".xmi")
270
+ end
271
+
272
+ # Check for SQLite database signature (QEA files)
273
+ if header[0..15] == "SQLite format 3\0"
274
+ ensure_default_parsers_loaded!
275
+ return parser_for_extension(".qea")
276
+ end
277
+ end
278
+
279
+ nil
280
+ end
281
+
282
+ # Get the best parser for a file using multiple detection methods
283
+ #
284
+ # @param file_path [String] Path to the file
285
+ # @param use_content_detection [Boolean] Whether to use content detection
286
+ # @return [Class, nil] Best parser class for the file
287
+ def best_parser_for_file(file_path, use_content_detection: true)
288
+ # First try extension-based detection
289
+ parser = parser_for_file(file_path)
290
+ return parser if parser
291
+
292
+ # Fall back to content detection if enabled
293
+ if use_content_detection
294
+ parser = detect_by_content(file_path)
295
+ return parser if parser
296
+ end
297
+
298
+ nil
299
+ end
300
+
301
+ private
302
+
303
+ # Ensure default parsers are loaded
304
+ #
305
+ # @return [void]
306
+ def ensure_default_parsers_loaded!
307
+ load_default_parsers unless @default_parsers_loaded
308
+ end
309
+
310
+ # Normalize file extension to lowercase with leading dot
311
+ #
312
+ # @param extension [String] File extension
313
+ # @return [String] Normalized extension
314
+ def normalize_extension(extension)
315
+ ext = extension.to_s.downcase
316
+ ext = ".#{ext}" unless ext.start_with?(".")
317
+ ext
318
+ end
319
+
320
+ # Validate file extension format
321
+ #
322
+ # @param extension [String] Extension to validate
323
+ # @raise [ArgumentError] if extension is invalid
324
+ def validate_extension!(extension)
325
+ if extension.nil? || extension.empty?
326
+ raise ArgumentError, "Extension cannot be nil or empty"
327
+ end
328
+
329
+ normalized = normalize_extension(extension)
330
+ unless normalized.match?(/^\.[a-z0-9]+(.[a-z0-9]+)?/)
331
+ raise ArgumentError, "Invalid extension format: #{extension}"
332
+ end
333
+ end
334
+
335
+ # Validate parser class implements required interface
336
+ #
337
+ # @param parser_class [Class] Parser class to validate
338
+ # @raise [ArgumentError] if parser class is invalid
339
+ def validate_parser_class!(parser_class) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
340
+ # Check if nil
341
+ if parser_class.nil?
342
+ raise ArgumentError, "Parser class cannot be nil"
343
+ end
344
+
345
+ # Check if it's a class
346
+ unless parser_class.is_a?(Class)
347
+ raise ArgumentError, "Parser must be a class"
348
+ end
349
+
350
+ unless parser_class < Parsers::BaseParser
351
+ raise ArgumentError,
352
+ "Parser class must inherit from BaseParser"
353
+ end
354
+ end
355
+
356
+ # Convert string class name to actual class constant
357
+ #
358
+ # @param class_name [String] Fully qualified class name
359
+ # @return [Class] The class constant
360
+ # @raise [NameError] if class cannot be found
361
+ def constantize_parser_class(class_name)
362
+ Transformations.constantize(class_name) ||
363
+ raise(NameError, "Cannot find class: #{class_name}")
364
+ end end
365
+ end
366
+ end