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