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,331 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Qea
5
+ module Validation
6
+ # Base class for all validators providing common validation
7
+ # infrastructure and helper methods
8
+ #
9
+ # @example Creating a custom validator
10
+ # class MyValidator < BaseValidator
11
+ # def validate
12
+ # check_required_field(:name)
13
+ # check_reference(:parent_id, :packages)
14
+ # end
15
+ # end
16
+ class BaseValidator
17
+ attr_reader :result, :context
18
+
19
+ # Creates a new validator
20
+ #
21
+ # @param result [ValidationResult] Result object to populate
22
+ # @param context [Hash] Validation context including database
23
+ # connection, document, etc.
24
+ def initialize(result:, context: {})
25
+ @result = result
26
+ @context = context
27
+ end
28
+
29
+ # Performs validation and returns the result
30
+ #
31
+ # This method should be overridden by subclasses
32
+ #
33
+ # @return [ValidationResult]
34
+ def validate
35
+ raise NotImplementedError,
36
+ "#{self.class} must implement #validate"
37
+ end
38
+
39
+ # Runs validation
40
+ # Result is populated in the shared @result object
41
+ #
42
+ # @return [void]
43
+ def call
44
+ validate
45
+ end
46
+
47
+ protected
48
+
49
+ # Returns the database connection from context
50
+ #
51
+ # @return [Ea::Qea::Database, nil]
52
+ def database
53
+ @context[:database]
54
+ end
55
+
56
+ # Returns the document being validated from context
57
+ #
58
+ # @return [Object, nil]
59
+ def document
60
+ @context[:document]
61
+ end
62
+
63
+ # Returns validation options from context
64
+ #
65
+ # @return [Hash]
66
+ def options
67
+ @context[:options] || {}
68
+ end
69
+
70
+ # Checks if a reference exists in a table
71
+ #
72
+ # @param entity_id [String, Integer] ID of the entity being validated
73
+ # @param entity_name [String] Name of the entity being validated
74
+ # @param entity_type [Symbol] Type of entity (e.g., :class,
75
+ # :association)
76
+ # @param field [String] Field name containing the reference
77
+ # @param reference_id [String, Integer] The referenced ID
78
+ # @param table [String] Table to check for reference existence
79
+ # @param id_column [String] ID column name in the reference table
80
+ # @param category [Symbol] Validation category
81
+ # @return [Boolean] True if reference exists, false otherwise
82
+ def check_reference_exists( # rubocop:disable Metrics/MethodLength,Metrics/ParameterLists
83
+ entity_id:,
84
+ entity_name:,
85
+ entity_type:,
86
+ field:,
87
+ reference_id:,
88
+ table:,
89
+ id_column: "ea_object_id",
90
+ category: :missing_reference
91
+ )
92
+ return true if reference_id.nil? || reference_id.to_s.empty?
93
+
94
+ exists = reference_exists?(table, id_column, reference_id)
95
+
96
+ unless exists
97
+ add_error(
98
+ category: category,
99
+ entity_type: entity_type,
100
+ entity_id: entity_id,
101
+ entity_name: entity_name,
102
+ field: field,
103
+ reference: reference_id.to_s,
104
+ message: "#{field} references non-existent #{table} entry",
105
+ )
106
+ end
107
+
108
+ exists
109
+ end
110
+
111
+ # Checks if a reference exists in the database
112
+ #
113
+ # @param table [String] Table name
114
+ # @param id_column [String] ID column name
115
+ # @param reference_id [String, Integer] The ID to check
116
+ # @return [Boolean]
117
+ def reference_exists?(table, id_column, reference_id)
118
+ return false unless database
119
+
120
+ collection = get_collection_for_table(table)
121
+ return false unless collection
122
+
123
+ collection.any? do |record|
124
+ record.public_send(id_column) == reference_id
125
+ end
126
+ end
127
+
128
+ # Maps table names to their corresponding collections in Database
129
+ #
130
+ # @param table [String] Table name (e.g., "t_package", "t_object")
131
+ # @return [Array, nil] Collection array or nil if table not mapped
132
+ def get_collection_for_table(table) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
133
+ case table
134
+ when "t_package"
135
+ database.packages
136
+ when "t_object"
137
+ database.objects.all
138
+ when "t_attribute"
139
+ database.attributes
140
+ when "t_operation"
141
+ database.operations
142
+ when "t_operationparams"
143
+ database.operation_params
144
+ when "t_connector"
145
+ database.connectors
146
+ when "t_diagram"
147
+ database.diagrams
148
+ when "t_diagramobjects"
149
+ database.diagram_objects
150
+ when "t_diagramlinks"
151
+ database.diagram_links
152
+ when "t_objectconstraint"
153
+ database.object_constraints
154
+ when "t_objectproperties"
155
+ database.object_properties
156
+ when "t_taggedvalue"
157
+ database.tagged_values
158
+ when "t_attributetag"
159
+ database.attribute_tags
160
+ when "t_xref"
161
+ database.xrefs
162
+ when "t_stereotypes"
163
+ database.stereotypes
164
+ when "t_datatypes"
165
+ database.datatypes
166
+ when "t_constrainttypes"
167
+ database.constraint_types
168
+ when "t_connectortypes"
169
+ database.connector_types
170
+ when "t_diagramtypes"
171
+ database.diagram_types
172
+ when "t_objecttypes"
173
+ database.object_types
174
+ when "t_statustypes"
175
+ database.status_types
176
+ when "t_complexitytypes"
177
+ database.complexity_types
178
+ end
179
+ end
180
+
181
+ # Adds an error to the validation result
182
+ #
183
+ # @param args [Hash] Message attributes
184
+ # @return [ValidationMessage]
185
+ def add_error(**args)
186
+ @result.add_error(**args)
187
+ end
188
+
189
+ # Adds a warning to the validation result
190
+ #
191
+ # @param args [Hash] Message attributes
192
+ # @return [ValidationMessage]
193
+ def add_warning(**args)
194
+ @result.add_warning(**args)
195
+ end
196
+
197
+ # Adds an info message to the validation result
198
+ #
199
+ # @param args [Hash] Message attributes
200
+ # @return [ValidationMessage]
201
+ def add_info(**args)
202
+ @result.add_info(**args)
203
+ end
204
+
205
+ # Checks if a value is present (not nil and not empty)
206
+ #
207
+ # @param value [Object] Value to check
208
+ # @return [Boolean]
209
+ def present?(value)
210
+ return false if value.nil?
211
+ return !value.empty? if value.is_a?(String) || value.is_a?(Array)
212
+
213
+ true
214
+ end
215
+
216
+ # Checks if a type name is a UML primitive type
217
+ #
218
+ # @param type [String, nil] Type name to check
219
+ # @return [Boolean] True if the type is a recognized UML primitive
220
+ PRIMITIVE_TYPES = %w[
221
+ String Integer Boolean Float Double Date Time DateTime
222
+ Real Decimal Byte Char Short Long UnsignedInt
223
+ UnsignedShort UnsignedLong PositiveInteger NonPositiveInteger
224
+ NegativeInteger NonNegativeInteger AnyURI Base64Binary
225
+ HexBinary Duration GYearMonth GYear GMonthDay GDay GMonth
226
+ int string boolean float double void object any
227
+ ].freeze
228
+
229
+ def primitive_type?(type)
230
+ return false unless type
231
+
232
+ PRIMITIVE_TYPES.include?(type.to_s)
233
+ end
234
+
235
+ # Finds entity name from database
236
+ #
237
+ # @param table [String] Table name
238
+ # @param id_column [String] ID column name
239
+ # @param name_column [String] Name column name
240
+ # @param id [String, Integer] Entity ID
241
+ # @return [String, nil] Entity name or nil if not found
242
+ def find_entity_name(table, id_column, name_column, id)
243
+ return nil unless database
244
+
245
+ collection = get_collection_for_table(table)
246
+ return nil unless collection
247
+
248
+ record = collection.find { |r| r.public_send(id_column) == id }
249
+ record&.public_send(name_column)
250
+ end
251
+
252
+ # Validates a collection of entities
253
+ #
254
+ # @param entities [Array] Collection of entities to validate
255
+ # @yield [entity] Validation block for each entity
256
+ # @return [ValidationResult]
257
+ def validate_each(entities, &)
258
+ entities.each(&)
259
+ @result
260
+ end
261
+
262
+ # Resolves package ID to full qualified path
263
+ #
264
+ # @param package_id [Integer] Package ID to resolve
265
+ # @return [String] Qualified path like
266
+ # "Root::ModelA::PackageB (package_id: 123)"
267
+ def resolve_package_path(package_id) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
268
+ return "Root" if package_id.nil? || package_id.zero?
269
+
270
+ path_parts = []
271
+ current_id = package_id
272
+ visited = Set.new
273
+
274
+ # Walk up the parent chain to build full path
275
+ while current_id && !current_id.zero?
276
+ break if visited.include?(current_id)
277
+
278
+ visited.add(current_id)
279
+ package = database.packages.find { |p| p.package_id == current_id }
280
+
281
+ if package
282
+ path_parts.unshift(package.name)
283
+ # Get parent_id for next iteration
284
+ current_id = package.parent_id
285
+ else
286
+ # Package not found, return what we have with ID
287
+ path_parts.unshift("Unknown")
288
+ break
289
+ end
290
+ end
291
+
292
+ if path_parts.empty?
293
+ "Unknown (package_id: #{package_id})"
294
+ else
295
+ "#{path_parts.join('::')} (package_id: #{package_id})"
296
+ end
297
+ end
298
+
299
+ # Resolves object to qualified class name including package path
300
+ #
301
+ # @param object_id [Integer] Object ID to resolve
302
+ # @param object_name [String, nil] Object name if already known
303
+ # @return [String] Qualified name like "Package::ClassName
304
+ # (object_id: 456)"
305
+ def resolve_class_path(object_id, object_name = nil) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
306
+ return "Unknown (object_id: #{object_id})" if object_id.nil?
307
+
308
+ # Get object from collection directly
309
+ object = database.objects.all.find do |obj|
310
+ obj.ea_object_id == object_id
311
+ end
312
+ return "Unknown (object_id: #{object_id})" unless object
313
+
314
+ # Get class name
315
+ class_name = object_name || object.name
316
+ package_id = object.package_id
317
+
318
+ if package_id && !package_id.zero?
319
+ package_path = resolve_package_path(package_id)
320
+ # Remove the package_id suffix from package path and add class
321
+ base_package_path = package_path.sub(/ \(package_id: \d+\)$/, "")
322
+ "#{base_package_path}::" \
323
+ "#{class_name || 'Unknown'} (object_id: #{object_id})"
324
+ else
325
+ "#{class_name || 'Unknown'} (object_id: #{object_id})"
326
+ end
327
+ end
328
+ end
329
+ end
330
+ end
331
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Qea
5
+ module Validation
6
+ # Validates class/object references and structure
7
+ class ClassValidator < BaseValidator
8
+ def validate
9
+ validate_package_references
10
+ validate_duplicate_names
11
+ validate_generalization_parents
12
+ end
13
+
14
+ private
15
+
16
+ def validate_package_references # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
17
+ objects.each do |obj|
18
+ next unless obj.package_id
19
+
20
+ unless package_exists?(obj.package_id)
21
+ class_path = resolve_class_path(obj.ea_object_id, obj.name)
22
+ result.add_error(
23
+ category: :missing_reference,
24
+ entity_type: :class,
25
+ entity_id: obj.ea_object_id.to_s,
26
+ entity_name: obj.name,
27
+ field: "package_id",
28
+ reference: obj.package_id.to_s,
29
+ message: "Package #{obj.package_id} does not exist",
30
+ location: class_path,
31
+ )
32
+ end
33
+ end
34
+ end
35
+
36
+ def validate_duplicate_names # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
37
+ objects_by_package = objects.group_by(&:package_id)
38
+
39
+ objects_by_package.each do |package_id, package_objects|
40
+ names = package_objects.map(&:name)
41
+ duplicates = names.select { |name| names.count(name) > 1 }.uniq
42
+
43
+ duplicates.each do |dup_name|
44
+ dup_objects = package_objects.select { |o| o.name == dup_name }
45
+ dup_objects.each do |obj|
46
+ class_path = resolve_class_path(obj.ea_object_id, obj.name)
47
+ package_path = if package_id
48
+ resolve_package_path(package_id)
49
+ else
50
+ "Root"
51
+ end
52
+ result.add_warning(
53
+ category: :duplicate,
54
+ entity_type: :class,
55
+ entity_id: obj.ea_object_id.to_s,
56
+ entity_name: obj.name,
57
+ field: "name",
58
+ message: "Duplicate class name '#{dup_name}' " \
59
+ "in #{package_path}",
60
+ location: class_path,
61
+ )
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ def validate_generalization_parents # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
68
+ connectors.select(&:generalization?).each do |gen|
69
+ parent_id = gen.end_object_id
70
+
71
+ unless object_exists?(parent_id)
72
+ child = objects.find { |o| o.ea_object_id == gen.start_object_id }
73
+ child_path = if child
74
+ resolve_class_path(child.ea_object_id,
75
+ child.name)
76
+ else
77
+ "Unknown"
78
+ end
79
+ result.add_error(
80
+ category: :missing_reference,
81
+ entity_type: :generalization,
82
+ entity_id: gen.connector_id.to_s,
83
+ entity_name: child&.name || "Unknown",
84
+ field: "end_object_id",
85
+ reference: parent_id.to_s,
86
+ message: "Generalization parent #{parent_id} does not exist",
87
+ location: child_path,
88
+ )
89
+ end
90
+ end
91
+ end
92
+
93
+ def objects
94
+ @objects ||= begin
95
+ # Use database objects for referential integrity checks
96
+ # Filter to only Class and Interface types (exclude Notes, etc.)
97
+ all_objects = context[:db_objects] || context[:objects] || []
98
+ all_objects.select { |obj| obj.uml_class? || obj.interface? }
99
+ end
100
+ end
101
+
102
+ def packages
103
+ # Use database packages for referential integrity checks
104
+ @packages ||= context[:db_packages] || context[:packages] || []
105
+ end
106
+
107
+ def connectors
108
+ @connectors ||= context[:connectors] || []
109
+ end
110
+
111
+ def package_exists?(package_id)
112
+ packages.any? { |p| p.package_id == package_id }
113
+ end
114
+
115
+ def object_exists?(object_id)
116
+ objects.any? { |o| o.ea_object_id == object_id }
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Qea
5
+ module Validation
6
+ # Detects circular dependencies and references
7
+ class CircularReferenceValidator < BaseValidator
8
+ def validate
9
+ detect_circular_packages
10
+ detect_circular_generalizations
11
+ end
12
+
13
+ private
14
+
15
+ def detect_circular_packages # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength
16
+ packages.each do |package|
17
+ next if package.root?
18
+
19
+ visited = Set.new([package.package_id])
20
+ current_id = package.parent_id
21
+
22
+ while current_id && !current_id.zero?
23
+ if visited.include?(current_id)
24
+ result.add_error(
25
+ category: :circular_reference,
26
+ entity_type: :package,
27
+ entity_id: package.package_id.to_s,
28
+ entity_name: package.name,
29
+ field: "parent_id",
30
+ message: "Circular package hierarchy: " \
31
+ "#{visited.to_a.join(' -> ')} -> #{current_id}",
32
+ )
33
+ break
34
+ end
35
+
36
+ visited << current_id
37
+ parent = packages.find { |p| p.package_id == current_id }
38
+ break unless parent
39
+
40
+ current_id = parent.parent_id
41
+ end
42
+ end
43
+ end
44
+
45
+ def detect_circular_generalizations # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
46
+ # Build generalization graph
47
+ generalizations = connectors.select(&:generalization?)
48
+
49
+ # For each class, check if it has circular inheritance
50
+ objects.each do |obj|
51
+ visited = Set.new([obj.ea_object_id])
52
+ queue = [obj.ea_object_id]
53
+
54
+ while queue.any?
55
+ current_id = queue.shift
56
+
57
+ # Find all parents of current object
58
+ parents = generalizations
59
+ .select { |g| g.start_object_id == current_id }
60
+ .map(&:end_object_id)
61
+
62
+ parents.each do |parent_id|
63
+ if visited.include?(parent_id)
64
+ # Found circular inheritance
65
+ result.add_error(
66
+ category: :circular_reference,
67
+ entity_type: :generalization,
68
+ entity_id: obj.ea_object_id.to_s,
69
+ entity_name: obj.name,
70
+ message: "Circular inheritance detected: " \
71
+ "#{format_inheritance_path(visited, parent_id)}",
72
+ )
73
+ next
74
+ end
75
+
76
+ visited << parent_id
77
+ queue << parent_id
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ def format_inheritance_path(visited_ids, circular_id) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
84
+ path = visited_ids.map do |id|
85
+ obj = objects.find { |o| o.ea_object_id == id }
86
+ obj&.name || id.to_s
87
+ end
88
+
89
+ circular_obj = objects.find { |o| o.ea_object_id == circular_id }
90
+ path << (circular_obj&.name || circular_id.to_s)
91
+
92
+ path.join(" -> ")
93
+ end
94
+
95
+ def packages
96
+ @packages ||= context[:db_packages] || []
97
+ end
98
+
99
+ def objects
100
+ @objects ||= context[:db_objects] || []
101
+ end
102
+
103
+ def connectors
104
+ @connectors ||= context[:connectors] || []
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end