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.
- checksums.yaml +4 -4
- data/CLAUDE.md +125 -0
- data/Rakefile +12 -4
- data/TODO.next/00-publish-blocking-bugs.md +74 -0
- data/TODO.next/01-standalone-ea-gem-identity.md +76 -0
- data/TODO.next/02-optional-lutaml-uml-dependency.md +47 -0
- data/TODO.next/03-slim-lutaml-uml.md +79 -0
- data/TODO.next/04-loader-registry-for-uml-repository.md +49 -0
- data/TODO.next/05-extract-shared-transformer-methods.md +14 -0
- data/TODO.next/06-deduplicate-stereotype-loading.md +17 -0
- data/TODO.next/07-transformer-registry-in-factory.md +20 -0
- data/TODO.next/08-connector-type-registry.md +27 -0
- data/TODO.next/09-element-renderer-registry.md +29 -0
- data/TODO.next/10-connector-renderer-lsp.md +18 -0
- data/TODO.next/11-consolidate-style-knowledge.md +33 -0
- data/TODO.next/12-data-driven-from-db-row.md +24 -0
- data/TODO.next/13-extract-duplicated-methods.md +17 -0
- data/TODO.next/14-remove-dead-code.md +10 -0
- data/TODO.next/15-narrow-exception-handling.md +39 -0
- data/TODO.next/16-repository-indexes.md +28 -0
- data/TODO.next/17-fix-spec-quality-and-coverage.md +32 -0
- data/TODO.next/18-xmi-tool-specific-parser-architecture.md +172 -0
- data/TODO.next/19-fix-ea-gemspec-dependency-declarations.md +56 -0
- data/TODO.next/20-ci-requires-unreleased-lutaml-uml.md +63 -0
- data/TODO.next/21-qeatoxmi-via-xmi-gem.md +340 -0
- data/TODO.next/22-strip-respond-to-from-qeatoxmi-specs.md +32 -0
- data/TODO.next/23-cleanup-idallocator.md +41 -0
- data/TODO.next/24-tighten-parity-specs.md +42 -0
- data/TODO.next/25-sparx-eaid-format-for-synthesized-ids.md +62 -0
- data/TODO.next/26-fix-uppervalue-lowervalue-count-gap.md +51 -0
- data/TODO.next/27-extract-cardinality-module.md +68 -0
- data/TODO.next/28-extract-xml-sanitizer.md +51 -0
- data/TODO.next/29-ocp-registry-for-classifier-builders.md +58 -0
- data/TODO.next/30-struct-return-for-association-end.md +37 -0
- data/TODO.next/31-idallocator-specs.md +27 -0
- data/TODO.next/32-phase2-gap-sentinel-specs.md +53 -0
- data/TODO.next/33-normalize-lower-cleanup.md +30 -0
- data/TODO.next/34-document-member-end-order-rt-prefix.md +29 -0
- data/TODO.next/35-walk-runstate-for-instance-slots.md +76 -0
- data/TODO.next/36-wire-interface-realization.md +50 -0
- data/TODO.next/37-visibility-returns-real-booleans.md +36 -0
- data/config/diagram_styles.yml +200 -0
- data/config/model_transformations.yml +266 -0
- data/config/qea_schema.yml +1024 -0
- data/docs/ea_to_uml_type_mapping.md +89 -0
- data/docs/xmi_qea_conversion_capabilities.md +99 -0
- data/examples/lur/20251010_current_plateau_v5.1.lur +0 -0
- data/examples/lur/basic.lur +0 -0
- data/examples/lur/test-output.lur +0 -0
- data/examples/lur/test.lur +0 -0
- data/examples/lur_basic_usage.rb +221 -0
- data/examples/lur_cli_workflow.rb +263 -0
- data/examples/lur_statistics.rb +326 -0
- data/examples/qea/20251010_current_plateau_v5.1.qea +0 -0
- data/examples/qea/ArcGISWorkspace_template.qea +0 -0
- data/examples/qea/README_qea_parser.adoc +230 -0
- data/examples/qea/UmlModel_template.qea +0 -0
- data/examples/qea/basic.qea +0 -0
- data/examples/qea/simple.qea +0 -0
- data/examples/qea/simple_example.qea +0 -0
- data/examples/qea/test.qea +0 -0
- data/examples/qea_standalone_query.rb +73 -0
- data/examples/qea_to_repository.rb +51 -0
- data/examples/smoke_test_real_qea.rb +81 -0
- data/exe/ea +7 -0
- data/lib/ea/cli/app.rb +72 -0
- data/lib/ea/cli/command/base.rb +80 -0
- data/lib/ea/cli/command/convert.rb +62 -0
- data/lib/ea/cli/command/diagrams.rb +81 -0
- data/lib/ea/cli/command/list.rb +61 -0
- data/lib/ea/cli/command/parse.rb +29 -0
- data/lib/ea/cli/command/stats.rb +20 -0
- data/lib/ea/cli/command/validate.rb +41 -0
- data/lib/ea/cli/command.rb +15 -0
- data/lib/ea/cli/error.rb +34 -0
- data/lib/ea/cli/output/formatter.rb +34 -0
- data/lib/ea/cli/output/json_formatter.rb +20 -0
- data/lib/ea/cli/output/table_formatter.rb +42 -0
- data/lib/ea/cli/output/yaml_formatter.rb +20 -0
- data/lib/ea/cli/output.rb +56 -0
- data/lib/ea/cli.rb +17 -0
- data/lib/ea/diagram/configuration.rb +379 -0
- data/lib/ea/diagram/element_renderers/base_renderer.rb +77 -0
- data/lib/ea/diagram/element_renderers/class_renderer.rb +323 -0
- data/lib/ea/diagram/element_renderers/connector_renderer.rb +41 -0
- data/lib/ea/diagram/element_renderers/package_renderer.rb +61 -0
- data/lib/ea/diagram/element_renderers.rb +43 -0
- data/lib/ea/diagram/extractor.rb +560 -0
- data/lib/ea/diagram/layout_engine.rb +170 -0
- data/lib/ea/diagram/path_builder.rb +202 -0
- data/lib/ea/diagram/style_parser.rb +42 -0
- data/lib/ea/diagram/style_resolver.rb +276 -0
- data/lib/ea/diagram/svg_renderer.rb +274 -0
- data/lib/ea/diagram/util.rb +73 -0
- data/lib/ea/diagram.rb +47 -0
- data/lib/ea/qea/benchmark.rb +210 -0
- data/lib/ea/qea/database.rb +308 -0
- data/lib/ea/qea/factory/association_builder.rb +203 -0
- data/lib/ea/qea/factory/association_transformer.rb +91 -0
- data/lib/ea/qea/factory/attribute_tag_transformer.rb +57 -0
- data/lib/ea/qea/factory/attribute_transformer.rb +93 -0
- data/lib/ea/qea/factory/base_transformer.rb +177 -0
- data/lib/ea/qea/factory/class_transformer.rb +116 -0
- data/lib/ea/qea/factory/constraint_transformer.rb +75 -0
- data/lib/ea/qea/factory/data_type_transformer.rb +77 -0
- data/lib/ea/qea/factory/diagram_transformer.rb +157 -0
- data/lib/ea/qea/factory/document_builder.rb +283 -0
- data/lib/ea/qea/factory/ea_to_uml_factory.rb +229 -0
- data/lib/ea/qea/factory/enum_transformer.rb +74 -0
- data/lib/ea/qea/factory/generalization_builder.rb +227 -0
- data/lib/ea/qea/factory/generalization_transformer.rb +98 -0
- data/lib/ea/qea/factory/instance_transformer.rb +68 -0
- data/lib/ea/qea/factory/object_property_transformer.rb +58 -0
- data/lib/ea/qea/factory/operation_transformer.rb +66 -0
- data/lib/ea/qea/factory/package_transformer.rb +145 -0
- data/lib/ea/qea/factory/reference_resolver.rb +99 -0
- data/lib/ea/qea/factory/stereotype_loader.rb +39 -0
- data/lib/ea/qea/factory/tagged_value_transformer.rb +38 -0
- data/lib/ea/qea/factory/transformer_registry.rb +80 -0
- data/lib/ea/qea/factory.rb +37 -0
- data/lib/ea/qea/file_detector.rb +178 -0
- data/lib/ea/qea/infrastructure/database_connection.rb +100 -0
- data/lib/ea/qea/infrastructure/schema_reader.rb +136 -0
- data/lib/ea/qea/infrastructure/table_reader.rb +224 -0
- data/lib/ea/qea/infrastructure.rb +12 -0
- data/lib/ea/qea/models/base_model.rb +59 -0
- data/lib/ea/qea/models/ea_attribute.rb +109 -0
- data/lib/ea/qea/models/ea_attribute_tag.rb +100 -0
- data/lib/ea/qea/models/ea_complexity_type.rb +79 -0
- data/lib/ea/qea/models/ea_connector.rb +160 -0
- data/lib/ea/qea/models/ea_connector_type.rb +60 -0
- data/lib/ea/qea/models/ea_constraint_type.rb +63 -0
- data/lib/ea/qea/models/ea_datatype.rb +104 -0
- data/lib/ea/qea/models/ea_diagram.rb +115 -0
- data/lib/ea/qea/models/ea_diagram_link.rb +78 -0
- data/lib/ea/qea/models/ea_diagram_object.rb +73 -0
- data/lib/ea/qea/models/ea_diagram_type.rb +56 -0
- data/lib/ea/qea/models/ea_document.rb +63 -0
- data/lib/ea/qea/models/ea_object.rb +223 -0
- data/lib/ea/qea/models/ea_object_constraint.rb +53 -0
- data/lib/ea/qea/models/ea_object_property.rb +87 -0
- data/lib/ea/qea/models/ea_object_type.rb +73 -0
- data/lib/ea/qea/models/ea_operation.rb +127 -0
- data/lib/ea/qea/models/ea_operation_param.rb +76 -0
- data/lib/ea/qea/models/ea_package.rb +78 -0
- data/lib/ea/qea/models/ea_script.rb +62 -0
- data/lib/ea/qea/models/ea_status_type.rb +66 -0
- data/lib/ea/qea/models/ea_stereotype.rb +57 -0
- data/lib/ea/qea/models/ea_tagged_value.rb +99 -0
- data/lib/ea/qea/models/ea_xref.rb +165 -0
- data/lib/ea/qea/models.rb +35 -0
- data/lib/ea/qea/repositories/base_repository.rb +225 -0
- data/lib/ea/qea/repositories/object_repository.rb +219 -0
- data/lib/ea/qea/repositories.rb +10 -0
- data/lib/ea/qea/services/configuration.rb +211 -0
- data/lib/ea/qea/services/database_loader.rb +191 -0
- data/lib/ea/qea/services.rb +10 -0
- data/lib/ea/qea/validation/association_validator.rb +73 -0
- data/lib/ea/qea/validation/attribute_validator.rb +91 -0
- data/lib/ea/qea/validation/base_validator.rb +331 -0
- data/lib/ea/qea/validation/class_validator.rb +121 -0
- data/lib/ea/qea/validation/database/circular_reference_validator.rb +109 -0
- data/lib/ea/qea/validation/database/orphan_validator.rb +153 -0
- data/lib/ea/qea/validation/database/referential_integrity_validator.rb +128 -0
- data/lib/ea/qea/validation/database.rb +16 -0
- data/lib/ea/qea/validation/diagram_validator.rb +112 -0
- data/lib/ea/qea/validation/formatters/json_formatter.rb +137 -0
- data/lib/ea/qea/validation/formatters/text_formatter.rb +235 -0
- data/lib/ea/qea/validation/formatters.rb +12 -0
- data/lib/ea/qea/validation/operation_validator.rb +71 -0
- data/lib/ea/qea/validation/package_validator.rb +111 -0
- data/lib/ea/qea/validation/validation_engine.rb +387 -0
- data/lib/ea/qea/validation/validation_message.rb +144 -0
- data/lib/ea/qea/validation/validation_result.rb +210 -0
- data/lib/ea/qea/validation/validator_registry.rb +134 -0
- data/lib/ea/qea/validation.rb +28 -0
- data/lib/ea/qea/verification/comparison_result.rb +264 -0
- data/lib/ea/qea/verification/document_normalizer.rb +169 -0
- data/lib/ea/qea/verification/document_verifier.rb +322 -0
- data/lib/ea/qea/verification/element_comparator.rb +277 -0
- data/lib/ea/qea/verification/structure_matcher.rb +287 -0
- data/lib/ea/qea/verification.rb +14 -0
- data/lib/ea/qea.rb +185 -0
- data/lib/ea/transformations/configuration.rb +333 -0
- data/lib/ea/transformations/format_registry.rb +366 -0
- data/lib/ea/transformations/parsers/base_parser.rb +482 -0
- data/lib/ea/transformations/parsers/qea_parser.rb +401 -0
- data/lib/ea/transformations/parsers/xmi_parser.rb +243 -0
- data/lib/ea/transformations/transformation_engine.rb +390 -0
- data/lib/ea/transformations.rb +85 -0
- data/lib/ea/transformers/qea_to_xmi/association_end.rb +19 -0
- data/lib/ea/transformers/qea_to_xmi/cardinality.rb +96 -0
- data/lib/ea/transformers/qea_to_xmi/context.rb +106 -0
- data/lib/ea/transformers/qea_to_xmi/guid_format.rb +56 -0
- data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +92 -0
- data/lib/ea/transformers/qea_to_xmi/run_state.rb +107 -0
- data/lib/ea/transformers/qea_to_xmi/transformer.rb +607 -0
- data/lib/ea/transformers/qea_to_xmi/visibility.rb +73 -0
- data/lib/ea/transformers/qea_to_xmi.rb +29 -0
- data/lib/ea/transformers/uml_to_xmi/id_generator.rb +54 -0
- data/lib/ea/transformers/uml_to_xmi/transformer.rb +152 -0
- data/lib/ea/transformers/uml_to_xmi/writer.rb +96 -0
- data/lib/ea/transformers/uml_to_xmi.rb +16 -0
- data/lib/ea/transformers.rb +34 -0
- data/lib/ea/version.rb +1 -1
- data/lib/ea/xmi/liquid_drops/association_drop.rb +56 -0
- data/lib/ea/xmi/liquid_drops/attribute_drop.rb +72 -0
- data/lib/ea/xmi/liquid_drops/cardinality_drop.rb +35 -0
- data/lib/ea/xmi/liquid_drops/connector_drop.rb +54 -0
- data/lib/ea/xmi/liquid_drops/constraint_drop.rb +29 -0
- data/lib/ea/xmi/liquid_drops/data_type_drop.rb +63 -0
- data/lib/ea/xmi/liquid_drops/dependency_drop.rb +36 -0
- data/lib/ea/xmi/liquid_drops/diagram_drop.rb +34 -0
- data/lib/ea/xmi/liquid_drops/enum_drop.rb +49 -0
- data/lib/ea/xmi/liquid_drops/enum_owned_literal_drop.rb +25 -0
- data/lib/ea/xmi/liquid_drops/generalization_attribute_drop.rb +87 -0
- data/lib/ea/xmi/liquid_drops/generalization_drop.rb +127 -0
- data/lib/ea/xmi/liquid_drops/klass_drop.rb +191 -0
- data/lib/ea/xmi/liquid_drops/operation_drop.rb +29 -0
- data/lib/ea/xmi/liquid_drops/package_drop.rb +108 -0
- data/lib/ea/xmi/liquid_drops/root_drop.rb +34 -0
- data/lib/ea/xmi/liquid_drops/source_target_drop.rb +43 -0
- data/lib/ea/xmi/lookup_service.rb +89 -0
- data/lib/ea/xmi/parser.rb +919 -0
- data/lib/ea/xmi.rb +35 -0
- data/lib/ea.rb +10 -1
- 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
|