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,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require "yaml"
5
+
6
+ module Ea
7
+ module Qea
8
+ module Services
9
+ # Configuration service loads and provides configuration from YAML file.
10
+ #
11
+ # This service uses lutaml-model for YAML parsing and provides access to
12
+ # QEA schema configuration including table definitions, type mappings,
13
+ # and transformation rules.
14
+ #
15
+ # @example Load configuration
16
+ # config = Configuration.load
17
+ # tables = config.enabled_tables
18
+ #
19
+ # @example Get table configuration
20
+ # table_cfg = config.table_config_for("t_object")
21
+ class Configuration < Lutaml::Model::Serializable
22
+ # Column definition model
23
+ class ColumnDefinition < Lutaml::Model::Serializable
24
+ attribute :name, :string
25
+ attribute :type, :string
26
+ attribute :primary, :boolean, default: -> { false }
27
+ attribute :nullable, :boolean, default: -> { true }
28
+ attribute :boolean, :boolean, default: -> { false }
29
+ attribute :default, :string
30
+
31
+ yaml do
32
+ map "name", to: :name
33
+ map "type", to: :type
34
+ map "primary", to: :primary
35
+ map "nullable", to: :nullable
36
+ map "boolean", to: :boolean
37
+ map "default", to: :default
38
+ end
39
+ end
40
+
41
+ # Table definition model
42
+ class TableDefinition < Lutaml::Model::Serializable
43
+ attribute :table_name, :string
44
+ attribute :enabled, :boolean, default: -> { true }
45
+ attribute :primary_key, :string
46
+ attribute :collection_name, :string
47
+ attribute :description, :string
48
+ attribute :columns, ColumnDefinition, collection: true
49
+
50
+ yaml do
51
+ map "table_name", to: :table_name
52
+ map "enabled", to: :enabled
53
+ map "primary_key", to: :primary_key
54
+ map "collection_name", to: :collection_name
55
+ map "description", to: :description
56
+ map "columns", to: :columns
57
+ end
58
+
59
+ # Get column definition by name
60
+ #
61
+ # @param column_name [String] The column name
62
+ # @return [ColumnDefinition, nil] The column definition or nil
63
+ def column_for(column_name)
64
+ columns&.find { |col| col.name == column_name }
65
+ end
66
+
67
+ # Check if a column is boolean type
68
+ #
69
+ # @param column_name [String] The column name
70
+ # @return [Boolean] true if column should be treated as boolean
71
+ def boolean_column?(column_name)
72
+ col = column_for(column_name)
73
+ col&.boolean == true
74
+ end
75
+ end
76
+
77
+ # Null handling configuration model
78
+ class NullHandling < Lutaml::Model::Serializable
79
+ attribute :strategy, :string
80
+ attribute :empty_string_as_null, :boolean, default: -> { true }
81
+ attribute :zero_as_null, :boolean, default: -> { false }
82
+
83
+ yaml do
84
+ map "strategy", to: :strategy
85
+ map "empty_string_as_null", to: :empty_string_as_null
86
+ map "zero_as_null", to: :zero_as_null
87
+ end
88
+ end
89
+
90
+ attribute :version, :string
91
+ attribute :description, :string
92
+ attribute :type_mappings, :string, collection: true
93
+ attribute :boolean_fields, :string, collection: true
94
+ attribute :null_handling, NullHandling
95
+ attribute :tables, TableDefinition, collection: true
96
+
97
+ yaml do
98
+ map "version", to: :version
99
+ map "description", to: :description
100
+ map "type_mappings", to: :type_mappings
101
+ map "boolean_fields", to: :boolean_fields
102
+ map "null_handling", to: :null_handling
103
+ map "tables", to: :tables
104
+ end
105
+
106
+ class << self
107
+ # Load configuration from YAML file
108
+ #
109
+ # @param config_path [String, nil] Path to configuration file
110
+ # Defaults to config/qea_schema.yml
111
+ # @return [Configuration] The loaded configuration
112
+ # @raise [Errno::ENOENT] if config file not found
113
+ # @raise [Lutaml::Model::Error] if YAML is invalid
114
+ def load(config_path = nil)
115
+ config_path ||= default_config_path
116
+
117
+ unless File.exist?(config_path)
118
+ raise Errno::ENOENT,
119
+ "Configuration file not found: #{config_path}"
120
+ end
121
+
122
+ yaml_content = File.read(config_path)
123
+ from_yaml(yaml_content)
124
+ end
125
+
126
+ # Get default configuration file path
127
+ #
128
+ # @return [String] Path to default config file
129
+ def default_config_path
130
+ File.expand_path("../../../../config/qea_schema.yml", __dir__)
131
+ end
132
+ end
133
+
134
+ # Get list of enabled tables
135
+ #
136
+ # @return [Array<TableDefinition>] Array of enabled table definitions
137
+ def enabled_tables
138
+ tables&.select(&:enabled) || []
139
+ end
140
+
141
+ # Get table configuration by table name
142
+ #
143
+ # @param table_name [String] The table name
144
+ # @return [TableDefinition, nil] The table definition
145
+ # or nil if not found
146
+ def table_config_for(table_name)
147
+ tables&.find { |t| t.table_name == table_name }
148
+ end
149
+
150
+ # Check if a table is enabled
151
+ #
152
+ # @param table_name [String] The table name
153
+ # @return [Boolean] true if table is enabled
154
+ def table_enabled?(table_name)
155
+ table = table_config_for(table_name)
156
+ table&.enabled == true
157
+ end
158
+
159
+ # Get all enabled table names
160
+ #
161
+ # @return [Array<String>] Array of enabled table names
162
+ def enabled_table_names
163
+ enabled_tables.map(&:table_name)
164
+ end
165
+
166
+ # Check if a field should be treated as boolean
167
+ #
168
+ # @param field_name [String] The field name
169
+ # @return [Boolean] true if field is in boolean_fields list
170
+ def boolean_field?(field_name)
171
+ boolean_fields&.include?(field_name) || false
172
+ end
173
+
174
+ # Get primary key for a table
175
+ #
176
+ # @param table_name [String] The table name
177
+ # @return [String, nil] The primary key column name
178
+ def primary_key_for(table_name)
179
+ table = table_config_for(table_name)
180
+ table&.primary_key
181
+ end
182
+
183
+ # Get collection name for a table
184
+ #
185
+ # @param table_name [String] The table name
186
+ # @return [String, nil] The collection name
187
+ def collection_name_for(table_name)
188
+ table = table_config_for(table_name)
189
+ table&.collection_name
190
+ end
191
+
192
+ # Convert empty strings to nil based on configuration
193
+ #
194
+ # @param value [String, nil] The value to convert
195
+ # @return [String, nil] The converted value
196
+ def convert_empty_string(value)
197
+ return value unless null_handling&.empty_string_as_null
198
+
199
+ value.nil? || value.empty? ? nil : value
200
+ end
201
+
202
+ # Check if zero should be treated as null
203
+ #
204
+ # @return [Boolean] true if zero should be converted to nil
205
+ def zero_as_null?
206
+ null_handling&.zero_as_null == true
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Qea
5
+ module Services
6
+ # DatabaseLoader orchestrates loading all EA tables into a Database
7
+ #
8
+ # This service loads all enabled tables from the QEA database,
9
+ # converts database rows to model instances, and populates a
10
+ # Database container with the results.
11
+ #
12
+ # @example Load a database
13
+ # loader = DatabaseLoader.new("model.qea")
14
+ # loader.on_progress do |table, current, total|
15
+ # puts "Loading #{table}: #{current}/#{total}"
16
+ # end
17
+ # database = loader.load
18
+ #
19
+ # @example Load a single table
20
+ # loader = DatabaseLoader.new("model.qea")
21
+ # objects = loader.load_table("t_object")
22
+ class DatabaseLoader
23
+ # @return [String] Path to QEA file
24
+ attr_reader :qea_path
25
+
26
+ # @return [Configuration] Configuration instance
27
+ attr_reader :config
28
+
29
+ # Table name to model class mapping
30
+ MODEL_CLASSES = {
31
+ "t_object" => Models::EaObject,
32
+ "t_attribute" => Models::EaAttribute,
33
+ "t_operation" => Models::EaOperation,
34
+ "t_operationparams" => Models::EaOperationParam,
35
+ "t_connector" => Models::EaConnector,
36
+ "t_package" => Models::EaPackage,
37
+ "t_diagram" => Models::EaDiagram,
38
+ "t_diagramobjects" => Models::EaDiagramObject,
39
+ "t_diagramlinks" => Models::EaDiagramLink,
40
+ "t_objectconstraint" => Models::EaObjectConstraint,
41
+ "t_taggedvalue" => Models::EaTaggedValue,
42
+ "t_objectproperties" => Models::EaObjectProperty,
43
+ "t_attributetag" => Models::EaAttributeTag,
44
+ "t_xref" => Models::EaXref,
45
+ "t_document" => Models::EaDocument,
46
+ "t_script" => Models::EaScript,
47
+ "t_stereotypes" => Models::EaStereotype,
48
+ "t_datatypes" => Models::EaDatatype,
49
+ "t_constrainttypes" => Models::EaConstraintType,
50
+ "t_connectortypes" => Models::EaConnectorType,
51
+ "t_diagramtypes" => Models::EaDiagramType,
52
+ "t_objecttypes" => Models::EaObjectType,
53
+ "t_statustypes" => Models::EaStatusType,
54
+ "t_complexitytypes" => Models::EaComplexityType,
55
+ }.freeze
56
+
57
+ # Initialize loader
58
+ #
59
+ # @param qea_path [String] Path to QEA file
60
+ # @param config [Configuration, nil] Optional configuration
61
+ def initialize(qea_path, config = nil)
62
+ @qea_path = qea_path
63
+ @config = config || Configuration.load
64
+ @progress_callback = nil
65
+ @connection = Infrastructure::DatabaseConnection.new(qea_path)
66
+ end
67
+
68
+ # Set progress callback
69
+ #
70
+ # @yield [table_name, current, total] Progress information
71
+ # @yieldparam table_name [String] Current table being loaded
72
+ # @yieldparam current [Integer] Records loaded so far
73
+ # @yieldparam total [Integer] Total records in table
74
+ # @return [self]
75
+ def on_progress(&block)
76
+ @progress_callback = block
77
+ self
78
+ end
79
+
80
+ # Load entire database
81
+ #
82
+ # @return [Database] Populated database instance
83
+ # @raise [Errno::ENOENT] if QEA file not found
84
+ # @raise [SQLite3::Exception] if database access fails
85
+ def load
86
+ # Connect but don't use with_connection - we need to keep it open
87
+ @connection.connect unless @connection.connected?
88
+ db = @connection.connection
89
+
90
+ database = Database.new(@qea_path, db)
91
+
92
+ @config.enabled_tables.each do |table_def|
93
+ table_name = table_def.table_name
94
+ collection_name = table_def.collection_name
95
+
96
+ records = load_table_records(db, table_name)
97
+ database.add_collection(collection_name, records)
98
+ end
99
+
100
+ database.freeze
101
+ end
102
+
103
+ # Load a single table
104
+ #
105
+ # @param table_name [String] Table name to load
106
+ # @return [Array] Array of model instances
107
+ # @raise [ArgumentError] if table not configured or not enabled
108
+ def load_table(table_name) # rubocop:disable Metrics/MethodLength
109
+ table_def = @config.table_config_for(table_name)
110
+ unless table_def
111
+ raise ArgumentError,
112
+ "Table #{table_name} not configured"
113
+ end
114
+ unless table_def.enabled
115
+ raise ArgumentError,
116
+ "Table #{table_name} not enabled"
117
+ end
118
+
119
+ @connection.with_connection do |db|
120
+ load_table_records(db, table_name)
121
+ end
122
+ end
123
+
124
+ # Get quick statistics without full loading
125
+ #
126
+ # @return [Hash<String, Integer>] Table names to record counts
127
+ def quick_stats
128
+ stats = {}
129
+
130
+ @connection.with_connection do |db|
131
+ @config.enabled_tables.each do |table_def|
132
+ reader = Infrastructure::TableReader.new(db, table_def.table_name)
133
+ stats[table_def.collection_name] = reader.count
134
+ end
135
+ end
136
+
137
+ stats
138
+ end
139
+
140
+ private
141
+
142
+ # Load records for a table
143
+ #
144
+ # @param db [SQLite3::Database] Database connection
145
+ # @param table_name [String] Table name
146
+ # @return [Array] Array of model instances
147
+ def load_table_records(db, table_name) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
148
+ model_class = MODEL_CLASSES[table_name]
149
+ unless model_class
150
+ raise ArgumentError, "No model class for table #{table_name}"
151
+ end
152
+
153
+ reader = Infrastructure::TableReader.new(db, table_name)
154
+ total = reader.count
155
+ records = []
156
+
157
+ rows = reader.all
158
+ rows.each_with_index do |row, index|
159
+ record = model_class.from_db_row(row)
160
+ records << record if record
161
+ if @progress_callback
162
+ report_progress(table_name, index + 1,
163
+ total)
164
+ end
165
+ rescue ArgumentError, TypeError, EncodingError => e
166
+ warn "Error loading record from #{table_name}: #{e.message}"
167
+ rescue StandardError => e
168
+ # Catch-all for record-level failures: skip bad records,
169
+ # don't abort the entire load. Connection-level errors propagate.
170
+ warn "Unexpected error loading record from #{table_name}: #{e.message}"
171
+ end
172
+
173
+ records
174
+ end
175
+
176
+ # Report progress to callback
177
+ #
178
+ # @param table_name [String] Current table
179
+ # @param current [Integer] Current record count
180
+ # @param total [Integer] Total records
181
+ def report_progress(table_name, current, total)
182
+ @progress_callback.call(table_name, current, total)
183
+ rescue StandardError => e
184
+ # Progress callbacks are user-supplied; isolate their failures from
185
+ # the load pipeline. The callback's contract is "best effort".
186
+ warn "Error in progress callback: #{e.message}"
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Qea
5
+ module Services
6
+ autoload :Configuration, "ea/qea/services/configuration"
7
+ autoload :DatabaseLoader, "ea/qea/services/database_loader"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Qea
5
+ module Validation
6
+ # Validates association/connector references
7
+ class AssociationValidator < BaseValidator
8
+ def validate
9
+ validate_object_references
10
+ validate_connector_endpoints
11
+ end
12
+
13
+ private
14
+
15
+ def validate_object_references
16
+ connectors.select(&:association?).each do |conn|
17
+ validate_start_object(conn)
18
+ validate_end_object(conn)
19
+ end
20
+ end
21
+
22
+ def validate_start_object(connector) # rubocop:disable Metrics/MethodLength
23
+ return if reference_exists?("t_object", "ea_object_id",
24
+ connector.start_object_id)
25
+
26
+ result.add_error(
27
+ category: :missing_reference,
28
+ entity_type: :association,
29
+ entity_id: connector.connector_id.to_s,
30
+ entity_name: connector.name || "Unnamed",
31
+ field: "start_object_id",
32
+ reference: connector.start_object_id.to_s,
33
+ message: "Start object #{connector.start_object_id} does not exist",
34
+ )
35
+ end
36
+
37
+ def validate_end_object(connector) # rubocop:disable Metrics/MethodLength
38
+ return if reference_exists?("t_object", "ea_object_id",
39
+ connector.end_object_id)
40
+
41
+ result.add_error(
42
+ category: :missing_reference,
43
+ entity_type: :association,
44
+ entity_id: connector.connector_id.to_s,
45
+ entity_name: connector.name || "Unnamed",
46
+ field: "end_object_id",
47
+ reference: connector.end_object_id.to_s,
48
+ message: "End object #{connector.end_object_id} does not exist",
49
+ )
50
+ end
51
+
52
+ def validate_connector_endpoints # rubocop:disable Metrics/MethodLength
53
+ connectors.each do |conn|
54
+ # Validate both endpoints exist
55
+ unless conn.start_object_id && conn.end_object_id
56
+ result.add_error(
57
+ category: :invalid_data,
58
+ entity_type: :connector,
59
+ entity_id: conn.connector_id.to_s,
60
+ entity_name: conn.name || "Unnamed",
61
+ message: "Connector missing start or end object reference",
62
+ )
63
+ end
64
+ end
65
+ end
66
+
67
+ def connectors
68
+ @connectors ||= context[:connectors] || []
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Qea
5
+ module Validation
6
+ # Validates attribute references and structure
7
+ class AttributeValidator < BaseValidator
8
+ def validate
9
+ validate_parent_object_references
10
+ validate_type_references
11
+ end
12
+
13
+ private
14
+
15
+ def validate_parent_object_references # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
16
+ attributes.each do |attr|
17
+ unless reference_exists?("t_object", "ea_object_id",
18
+ attr.ea_object_id)
19
+ parent = database&.objects&.all&.find do |o|
20
+ o.ea_object_id == attr.ea_object_id
21
+ end
22
+ attr_location = if parent
23
+ "#{resolve_class_path(
24
+ parent.ea_object_id,
25
+ parent.name,
26
+ )}::#{attr.name}"
27
+ else
28
+ "Unknown::#{attr.name} " \
29
+ "(attribute_id: #{attr.id})"
30
+ end
31
+ result.add_error(
32
+ category: :missing_reference,
33
+ entity_type: :attribute,
34
+ entity_id: attr.id.to_s,
35
+ entity_name: attr.name,
36
+ field: "ea_object_id",
37
+ reference: attr.ea_object_id.to_s,
38
+ message: "Parent object #{attr.ea_object_id} does not exist",
39
+ location: attr_location,
40
+ )
41
+ end
42
+ end
43
+ end
44
+
45
+ def validate_type_references # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
46
+ attributes.each do |attr|
47
+ next unless attr.classifier && !attr.classifier.empty?
48
+
49
+ # Classifier can be either an object ID or a primitive type name
50
+ next if primitive_type?(attr.classifier)
51
+ next if classifier_exists?(attr.classifier)
52
+
53
+ parent = database&.objects&.all&.find do |o|
54
+ o.ea_object_id == attr.ea_object_id
55
+ end
56
+ attr_location = if parent
57
+ "#{resolve_class_path(parent.ea_object_id,
58
+ parent.name)}::#{attr.name}"
59
+ else
60
+ "Unknown::#{attr.name} (attribute_id: #{attr.id})"
61
+ end
62
+ result.add_warning(
63
+ category: :missing_reference,
64
+ entity_type: :attribute,
65
+ entity_id: attr.id.to_s,
66
+ entity_name: "#{parent&.name}.#{attr.name}",
67
+ field: "classifier",
68
+ reference: attr.classifier,
69
+ message: "Classifier '#{attr.classifier}' not found",
70
+ location: attr_location,
71
+ )
72
+ end
73
+ end
74
+
75
+ def attributes
76
+ @attributes ||= context[:attributes] || []
77
+ end
78
+
79
+ def classifier_exists?(classifier_id)
80
+ return false unless database
81
+
82
+ # Classifier field contains object_id, not name
83
+ database.objects.all.any? do |o|
84
+ o.ea_object_id.to_s == classifier_id.to_s
85
+ end
86
+ end
87
+
88
+ end
89
+ end
90
+ end
91
+ end