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
data/lib/ea/qea.rb ADDED
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Qea
5
+ autoload :Infrastructure, "ea/qea/infrastructure"
6
+ autoload :Services, "ea/qea/services"
7
+ autoload :Models, "ea/qea/models"
8
+ autoload :Factory, "ea/qea/factory"
9
+ autoload :Validation, "ea/qea/validation"
10
+ autoload :Verification, "ea/qea/verification"
11
+ autoload :Database, "ea/qea/database"
12
+ autoload :Repositories, "ea/qea/repositories"
13
+ autoload :Benchmark, "ea/qea/benchmark"
14
+ autoload :FileDetector, "ea/qea/file_detector"
15
+
16
+ class << self
17
+ # Get the current configuration
18
+ #
19
+ # @return [Services::Configuration] The loaded configuration
20
+ def configuration
21
+ @configuration ||= Services::Configuration.load
22
+ end
23
+
24
+ # Set a custom configuration
25
+ #
26
+ # @param config [Services::Configuration] The configuration to use
27
+ def configuration=(config)
28
+ @configuration = config
29
+ end
30
+
31
+ # Reload configuration from file
32
+ #
33
+ # @param config_path [String, nil] Optional custom config path
34
+ # @return [Services::Configuration] The reloaded configuration
35
+ def reload_configuration(config_path = nil)
36
+ @configuration = Services::Configuration.load(config_path)
37
+ end
38
+
39
+ # Connect to a QEA file
40
+ #
41
+ # @param file_path [String] Path to the .qea file
42
+ # @return [Infrastructure::DatabaseConnection] The connection object
43
+ def connect(file_path)
44
+ Infrastructure::DatabaseConnection.new(file_path)
45
+ end
46
+
47
+ # Open a QEA file and yield the connection
48
+ #
49
+ # @param file_path [String] Path to the .qea file
50
+ # @yield [Infrastructure::DatabaseConnection] The connection object
51
+ # @return [Object] The result of the block
52
+ def open(file_path)
53
+ connection = connect(file_path)
54
+ yield connection
55
+ ensure
56
+ connection&.close if connection&.connected?
57
+ end
58
+
59
+ # Get schema information from a QEA file
60
+ #
61
+ # @param file_path [String] Path to the .qea file
62
+ # @return [Hash] Schema information including tables and row counts
63
+ def schema_info(file_path)
64
+ connection = connect(file_path)
65
+ connection.with_connection do |db|
66
+ reader = Infrastructure::SchemaReader.new(db)
67
+ {
68
+ tables: reader.tables,
69
+ statistics: reader.statistics,
70
+ }
71
+ end
72
+ ensure
73
+ connection&.close if connection&.connected?
74
+ end
75
+
76
+ # Load complete database with all tables and models (standalone API)
77
+ #
78
+ # This is the primary standalone entry point — returns EA-native data
79
+ # without requiring lutaml-uml.
80
+ #
81
+ # @param qea_path [String] Path to the .qea file
82
+ # @param config [Services::Configuration, nil] Optional custom config
83
+ # @return [Database] Loaded database with all collections
84
+ def load(qea_path, config = nil, &progress_callback)
85
+ load_database(qea_path, config, &progress_callback)
86
+ end
87
+
88
+ # Load complete database with all tables and models
89
+ #
90
+ # @param qea_path [String] Path to the .qea file
91
+ # @param config [Services::Configuration, nil]
92
+ # @return [Database] Loaded database with all collections
93
+ def load_database(qea_path, config = nil, &progress_callback)
94
+ loader = Services::DatabaseLoader.new(qea_path, config)
95
+ loader.on_progress(&progress_callback) if progress_callback
96
+ loader.load
97
+ end
98
+
99
+ # Get quick database statistics without full loading
100
+ #
101
+ # @param qea_path [String] Path to the .qea file
102
+ # @param config [Services::Configuration, nil]
103
+ # @return [Hash<String, Integer>] Collection names to record counts
104
+ def database_info(qea_path, config = nil)
105
+ loader = Services::DatabaseLoader.new(qea_path, config)
106
+ loader.quick_stats
107
+ end
108
+
109
+ # Convert an EA database to a UML Document (requires lutaml-uml)
110
+ #
111
+ # @param database [Database] Loaded EA database
112
+ # @param options [Hash] Transformation options
113
+ # @return [Lutaml::Uml::Document] UML document
114
+ # @raise [Ea::Error] if lutaml-uml is not available
115
+ def to_uml(database, options = {})
116
+ require_uml!
117
+ factory = Factory::EaToUmlFactory.new(database, options)
118
+ factory.create_document
119
+ end
120
+
121
+ # Parse QEA file to UML Document (convenience: load + to_uml)
122
+ #
123
+ # Requires lutaml-uml for the UML conversion.
124
+ #
125
+ # @param qea_path [String] Path to the .qea file
126
+ # @param options [Hash] Transformation options
127
+ # @return [Lutaml::Uml::Document, Hash] Document, or hash with
128
+ # :document and :validation_result
129
+ # @raise [Ea::Error] if lutaml-uml is not available
130
+ def parse(qea_path, options = {}) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
131
+ require_uml!
132
+ config = options.delete(:config)
133
+ validate = options.delete(:validate)
134
+
135
+ loader = Services::DatabaseLoader.new(qea_path, config)
136
+ ea_database = loader.load
137
+
138
+ begin
139
+ factory = Factory::EaToUmlFactory.new(ea_database, options)
140
+ document = factory.create_document
141
+
142
+ if validate
143
+ engine = Validation::ValidationEngine.new(
144
+ document,
145
+ database: ea_database,
146
+ **options,
147
+ )
148
+ validation_result = engine.validate
149
+
150
+ {
151
+ document: document,
152
+ validation_result: validation_result,
153
+ }
154
+ else
155
+ document
156
+ end
157
+ ensure
158
+ ea_database.close_connection unless validate
159
+ end
160
+ end
161
+
162
+ private
163
+
164
+ # Ensure lutaml-uml is available for UML conversion operations
165
+ #
166
+ # @raise [Ea::Error] if lutaml-uml is not loaded
167
+ def require_uml!
168
+ return if defined?(Lutaml::Uml::Document)
169
+
170
+ begin
171
+ require "lutaml/uml"
172
+ rescue LoadError
173
+ raise Ea::Error,
174
+ "lutaml-uml is required for UML conversion. " \
175
+ "Add gem 'lutaml-uml' to your Gemfile."
176
+ end
177
+
178
+ return if defined?(Lutaml::Uml::Document)
179
+
180
+ raise Ea::Error,
181
+ "lutaml-uml failed to load Lutaml::Uml::Document."
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,333 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require "yaml"
5
+
6
+ module Ea
7
+ module Transformations
8
+ # Configuration service for model transformations using external YAML
9
+ # configuration.
10
+ #
11
+ # This class follows the Dependency Inversion Principle by allowing external
12
+ # configuration instead of hardcoded behavior. It uses lutaml-model for
13
+ # structured YAML parsing and validation.
14
+ #
15
+ # @example Load default configuration
16
+ # config = Configuration.load
17
+ # puts config.enabled_formats
18
+ #
19
+ # @example Load custom configuration
20
+ # config = Configuration.load("my_config.yml")
21
+ # parser_config = config.parser_config_for("xmi")
22
+ class Configuration < Lutaml::Model::Serializable
23
+ # Parser configuration model
24
+ class ParserConfig < Lutaml::Model::Serializable
25
+ attribute :format, :string
26
+ attribute :extension, :string
27
+ attribute :parser_class, :string
28
+ attribute :enabled, :boolean, default: -> { true }
29
+ attribute :priority, :integer, default: -> { 100 }
30
+ attribute :description, :string
31
+ attribute :options, :string, collection: true
32
+
33
+ yaml do
34
+ map "format", to: :format
35
+ map "extension", to: :extension
36
+ map "parser_class", to: :parser_class
37
+ map "enabled", to: :enabled
38
+ map "priority", to: :priority
39
+ map "description", to: :description
40
+ map "options", to: :options
41
+ end
42
+
43
+ # Check if this parser handles the given extension
44
+ #
45
+ # @param ext [String] File extension (e.g., ".xmi")
46
+ # @return [Boolean] true if this parser handles the extension
47
+ def handles_extension?(ext)
48
+ extension == ext.downcase
49
+ end
50
+ end
51
+
52
+ # Transformation options model
53
+ class TransformationOptions < Lutaml::Model::Serializable
54
+ attribute :validate_output, :boolean, default: -> { false }
55
+ attribute :include_diagrams, :boolean, default: -> { true }
56
+ attribute :preserve_ids, :boolean, default: -> { true }
57
+ attribute :resolve_references, :boolean, default: -> { true }
58
+ attribute :strict_mode, :boolean, default: -> { false }
59
+
60
+ yaml do
61
+ map "validate_output", to: :validate_output
62
+ map "include_diagrams", to: :include_diagrams
63
+ map "preserve_ids", to: :preserve_ids
64
+ map "resolve_references", to: :resolve_references
65
+ map "strict_mode", to: :strict_mode
66
+ end
67
+ end
68
+
69
+ # Format detection rules model
70
+ class FormatDetection < Lutaml::Model::Serializable
71
+ attribute :use_file_extension, :boolean, default: -> { true }
72
+ attribute :use_content_sniffing, :boolean, default: -> { true }
73
+ attribute :fallback_parser, :string
74
+ attribute :magic_bytes, :string, collection: true
75
+
76
+ yaml do
77
+ map "use_file_extension", to: :use_file_extension
78
+ map "use_content_sniffing", to: :use_content_sniffing
79
+ map "fallback_parser", to: :fallback_parser
80
+ map "magic_bytes", to: :magic_bytes
81
+ end
82
+ end
83
+
84
+ # Error handling configuration
85
+ class ErrorHandling < Lutaml::Model::Serializable
86
+ attribute :strategy, :string, default: -> { "continue" }
87
+ attribute :log_errors, :boolean, default: -> { true }
88
+ attribute :max_errors, :integer, default: -> { 10 }
89
+ attribute :fail_fast, :boolean, default: -> { false }
90
+
91
+ yaml do
92
+ map "strategy", to: :strategy
93
+ map "log_errors", to: :log_errors
94
+ map "max_errors", to: :max_errors
95
+ map "fail_fast", to: :fail_fast
96
+ end
97
+ end
98
+
99
+ attribute :version, :string
100
+ attribute :description, :string
101
+ attribute :parsers, ParserConfig, collection: true
102
+ attribute :transformation_options, TransformationOptions
103
+ attribute :format_detection, FormatDetection
104
+ attribute :error_handling, ErrorHandling
105
+
106
+ yaml do
107
+ map "version", to: :version
108
+ map "description", to: :description
109
+ map "parsers", to: :parsers
110
+ map "transformation_options", to: :transformation_options
111
+ map "format_detection", to: :format_detection
112
+ map "error_handling", to: :error_handling
113
+ end
114
+
115
+ class << self
116
+ # Load configuration from YAML file
117
+ #
118
+ # @param config_path [String, nil] Path to configuration file
119
+ # Defaults to config/model_transformations.yml
120
+ # @return [Configuration] The loaded configuration
121
+ # @raise [Errno::ENOENT] if config file not found
122
+ # @raise [Lutaml::Model::Error] if YAML is invalid
123
+ def load(config_path = nil)
124
+ config_path ||= default_config_path
125
+
126
+ unless File.exist?(config_path)
127
+ # Create default configuration if none exists
128
+ return create_default_configuration
129
+ end
130
+
131
+ yaml_content = File.read(config_path)
132
+ from_yaml(yaml_content)
133
+ end
134
+
135
+ # Get default configuration file path
136
+ #
137
+ # @return [String] Path to default config file
138
+ def default_config_path
139
+ File.expand_path("../../../config/model_transformations.yml", __dir__)
140
+ end
141
+
142
+ # Create default configuration when no config file exists
143
+ #
144
+ # @return [Configuration] Default configuration instance
145
+ def create_default_configuration
146
+ new.tap do |config|
147
+ config.version = "1.0"
148
+ config.description = "Default Model Transformations Configuration"
149
+
150
+ # Default parsers
151
+ config.parsers = [
152
+ create_xmi_parser_config,
153
+ create_qea_parser_config,
154
+ ]
155
+
156
+ # Default options
157
+ config.transformation_options = TransformationOptions.new
158
+ config.format_detection = FormatDetection.new
159
+ config.error_handling = ErrorHandling.new
160
+ end
161
+ end
162
+
163
+ private
164
+
165
+ # Create default XMI parser configuration
166
+ #
167
+ # @return [ParserConfig] XMI parser configuration
168
+ def create_xmi_parser_config
169
+ ParserConfig.new.tap do |parser|
170
+ parser.format = "xmi"
171
+ parser.extension = ".xmi"
172
+ parser.parser_class = "Ea::Transformations::Parsers::XmiParser"
173
+ parser.enabled = true
174
+ parser.priority = 100
175
+ parser.description = "XML Metadata Interchange parser"
176
+ parser.options = ["validate_xml", "resolve_references"]
177
+ end
178
+ end
179
+
180
+ # Create default QEA parser configuration
181
+ #
182
+ # @return [ParserConfig] QEA parser configuration
183
+ def create_qea_parser_config
184
+ ParserConfig.new.tap do |parser|
185
+ parser.format = "qea"
186
+ parser.extension = ".qea"
187
+ parser.parser_class = "Ea::Transformations::Parsers::QeaParser"
188
+ parser.enabled = true
189
+ parser.priority = 90
190
+ parser.description = "Enterprise Architect database parser"
191
+ parser.options = ["include_diagrams", "resolve_references"]
192
+ end
193
+ end
194
+ end
195
+
196
+ # Get list of enabled parsers, sorted by priority
197
+ #
198
+ # @return [Array<ParserConfig>] Array of enabled parser configurations
199
+ def enabled_parsers
200
+ parsers&.select(&:enabled)&.sort_by { |p| -p.priority } || []
201
+ end
202
+
203
+ # Get parser configuration by format name
204
+ #
205
+ # @param format [String] The format name (e.g., "xmi", "qea")
206
+ # @return [ParserConfig, nil] The parser configuration or nil if not found
207
+ def parser_config_for(format)
208
+ parsers&.find { |p| p.format == format.downcase }
209
+ end
210
+
211
+ # Get parser configuration by file extension
212
+ #
213
+ # @param extension [String] The file extension (e.g., ".xmi", ".qea")
214
+ # @return [ParserConfig, nil] The parser configuration or nil if not found
215
+ def parser_config_for_extension(extension)
216
+ normalized_ext = extension.downcase
217
+ unless normalized_ext.start_with?(".")
218
+ normalized_ext = ".#{normalized_ext}"
219
+ end
220
+
221
+ enabled_parsers.find { |p| p.handles_extension?(normalized_ext) }
222
+ end
223
+
224
+ # Check if a format is enabled
225
+ #
226
+ # @param format [String] The format name
227
+ # @return [Boolean] true if format is enabled
228
+ def format_enabled?(format)
229
+ parser = parser_config_for(format)
230
+ parser&.enabled == true
231
+ end
232
+
233
+ # Get all enabled format names
234
+ #
235
+ # @return [Array<String>] Array of enabled format names
236
+ def enabled_formats
237
+ enabled_parsers.map(&:format)
238
+ end
239
+
240
+ # Get all supported file extensions
241
+ #
242
+ # @return [Array<String>] Array of supported extensions
243
+ def supported_extensions
244
+ enabled_parsers.filter_map(&:extension)
245
+ end
246
+
247
+ # Check if content sniffing is enabled
248
+ #
249
+ # @return [Boolean] true if content sniffing should be used
250
+ def content_sniffing_enabled?
251
+ format_detection&.use_content_sniffing == true
252
+ end
253
+
254
+ # Check if file extension detection is enabled
255
+ #
256
+ # @return [Boolean] true if file extension should be used for detection
257
+ def file_extension_detection_enabled?
258
+ format_detection&.use_file_extension == true
259
+ end
260
+
261
+ # Get fallback parser when format detection fails
262
+ #
263
+ # @return [String, nil] The fallback parser class name
264
+ def fallback_parser
265
+ format_detection&.fallback_parser
266
+ end
267
+
268
+ # Get transformation options with defaults
269
+ #
270
+ # @return [TransformationOptions] Transformation options
271
+ def transformation_options
272
+ @transformation_options ||= TransformationOptions.new
273
+ end
274
+
275
+ # Get error handling configuration with defaults
276
+ #
277
+ # @return [ErrorHandling] Error handling configuration
278
+ def error_handling
279
+ @error_handling ||= ErrorHandling.new
280
+ end
281
+
282
+ # Merge with another configuration (this takes precedence)
283
+ #
284
+ # @param other [Configuration] Configuration to merge with
285
+ # @return [Configuration] New merged configuration
286
+ def merge(other) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
287
+ merged = self.class.new
288
+
289
+ # Basic attributes
290
+ merged.version = version || other.version
291
+ merged.description = description || other.description
292
+
293
+ # Merge parsers (this config takes precedence)
294
+ merged.parsers = merge_parsers(other.parsers)
295
+
296
+ # Use this config's options, fallback to other
297
+ merged.transformation_options = transformation_options ||
298
+ other.transformation_options
299
+ merged.format_detection = format_detection || other.format_detection
300
+ merged.error_handling = error_handling || other.error_handling
301
+
302
+ merged
303
+ end
304
+
305
+ private
306
+
307
+ # Merge parser configurations
308
+ #
309
+ # @param other_parsers [Array<ParserConfig>] Other parsers to merge
310
+ # @return [Array<ParserConfig>] Merged parser list
311
+ def merge_parsers(other_parsers) # rubocop:disable Metrics/MethodLength
312
+ return parsers unless other_parsers
313
+ return other_parsers unless parsers
314
+
315
+ # Create a hash of this config's parsers by format
316
+ our_parsers = {}
317
+ parsers.each do |parser|
318
+ our_parsers[parser.format] = parser
319
+ end
320
+
321
+ # Add other parsers that we don't have
322
+ merged = parsers.dup
323
+ other_parsers.each do |other_parser|
324
+ unless our_parsers.key?(other_parser.format)
325
+ merged << other_parser
326
+ end
327
+ end
328
+
329
+ merged
330
+ end
331
+ end
332
+ end
333
+ end