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,482 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Transformations
|
|
5
|
+
module Parsers
|
|
6
|
+
# Base parser interface defining the contract for all model format
|
|
7
|
+
# parsers.
|
|
8
|
+
#
|
|
9
|
+
# This abstract base class implements the Template Method pattern and
|
|
10
|
+
# follows the Liskov Substitution Principle - all concrete parsers
|
|
11
|
+
# must be substitutable for this base class.
|
|
12
|
+
#
|
|
13
|
+
# Concrete parsers must implement:
|
|
14
|
+
# - parse_internal: Core parsing logic
|
|
15
|
+
# - supported_extensions: List of supported file extensions
|
|
16
|
+
# - format_name: Human-readable format name
|
|
17
|
+
#
|
|
18
|
+
# @abstract Subclass and implement the abstract methods
|
|
19
|
+
class BaseParser
|
|
20
|
+
# @return [Configuration] Parser configuration
|
|
21
|
+
attr_reader :configuration
|
|
22
|
+
|
|
23
|
+
# @return [Hash] Parsing options
|
|
24
|
+
attr_reader :options
|
|
25
|
+
|
|
26
|
+
# @return [Float, nil] Duration of last parse in seconds
|
|
27
|
+
attr_reader :last_duration
|
|
28
|
+
|
|
29
|
+
# Initialize parser with configuration and options
|
|
30
|
+
#
|
|
31
|
+
# @param configuration [Configuration] Transformation configuration
|
|
32
|
+
# @param options [Hash] Parsing options
|
|
33
|
+
def initialize(configuration: nil, options: {})
|
|
34
|
+
@configuration = configuration
|
|
35
|
+
@options = default_options.merge(options)
|
|
36
|
+
@errors = []
|
|
37
|
+
@warnings = []
|
|
38
|
+
@parse_stats = {
|
|
39
|
+
total_parses: 0,
|
|
40
|
+
successful_parses: 0,
|
|
41
|
+
failed_parses: 0,
|
|
42
|
+
total_duration: 0,
|
|
43
|
+
durations: [],
|
|
44
|
+
}
|
|
45
|
+
@last_duration = nil
|
|
46
|
+
@stats_mutex = Mutex.new
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Parse a model file into a UML document
|
|
50
|
+
#
|
|
51
|
+
# This is the main public interface method that implements the
|
|
52
|
+
# Template Method pattern. It handles common concerns like validation,
|
|
53
|
+
# error handling, and post-processing.
|
|
54
|
+
#
|
|
55
|
+
# @param file_path [String] Path to the model file
|
|
56
|
+
# @return [Lutaml::Uml::Document] Parsed UML document
|
|
57
|
+
# @raise [ParseError] if parsing fails
|
|
58
|
+
def parse(file_path)
|
|
59
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
60
|
+
@stats_mutex.synchronize { @parse_stats[:total_parses] += 1 }
|
|
61
|
+
parse_succeeded = false
|
|
62
|
+
parse_handled = false
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
validate_file!(file_path) if should_validate_input?
|
|
66
|
+
clear_errors_and_warnings
|
|
67
|
+
|
|
68
|
+
parse_succeeded, result = execute_parse(file_path)
|
|
69
|
+
parse_handled = !parse_succeeded
|
|
70
|
+
result
|
|
71
|
+
ensure
|
|
72
|
+
record_parse_stats(parse_succeeded, parse_handled, start_time)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def execute_parse(file_path)
|
|
77
|
+
before_parse(file_path)
|
|
78
|
+
document = parse_internal(file_path)
|
|
79
|
+
document = after_parse(document, file_path)
|
|
80
|
+
validate_output!(document) if should_validate_output?
|
|
81
|
+
|
|
82
|
+
@stats_mutex.synchronize { @parse_stats[:successful_parses] += 1 }
|
|
83
|
+
[true, document]
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
[false, handle_parsing_error(e, file_path)]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def record_parse_stats(succeeded, handled, start_time)
|
|
89
|
+
unless succeeded || handled
|
|
90
|
+
@stats_mutex.synchronize { @parse_stats[:failed_parses] += 1 }
|
|
91
|
+
end
|
|
92
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
93
|
+
@last_duration = duration
|
|
94
|
+
@stats_mutex.synchronize do
|
|
95
|
+
@parse_stats[:total_duration] += duration
|
|
96
|
+
@parse_stats[:durations] << duration
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Check if this parser can handle the given file
|
|
101
|
+
#
|
|
102
|
+
# @param file_path [String] Path to the file
|
|
103
|
+
# @return [Boolean] true if parser can handle the file
|
|
104
|
+
def can_parse?(file_path) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
105
|
+
extension = File.extname(file_path).downcase
|
|
106
|
+
return true if supported_extensions.include?(extension)
|
|
107
|
+
|
|
108
|
+
if File.exist?(file_path)
|
|
109
|
+
File.open(file_path, "rb") do |file|
|
|
110
|
+
header = file.read(1024) # Read first 1KB
|
|
111
|
+
return false if header.nil? || header.empty?
|
|
112
|
+
|
|
113
|
+
content_patterns.each do |pattern|
|
|
114
|
+
return true if header.match?(pattern)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
false
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Get parser format name
|
|
123
|
+
#
|
|
124
|
+
# @return [String] Human-readable format name
|
|
125
|
+
# @abstract Implement in subclass
|
|
126
|
+
def format_name
|
|
127
|
+
raise NotImplementedError, "Subclasses must implement #format_name"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Get list of supported file extensions
|
|
131
|
+
#
|
|
132
|
+
# @return [Array<String>] List of extensions (e.g., [".xmi", ".xml"])
|
|
133
|
+
# @abstract Implement in subclass
|
|
134
|
+
def supported_extensions
|
|
135
|
+
raise NotImplementedError,
|
|
136
|
+
"Subclasses must implement #supported_extensions"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Default parser priority
|
|
140
|
+
def priority
|
|
141
|
+
100
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def content_patterns
|
|
145
|
+
[]
|
|
146
|
+
end
|
|
147
|
+
# Check if parser has any errors
|
|
148
|
+
#
|
|
149
|
+
# @return [Boolean] true if there are parsing errors
|
|
150
|
+
def has_errors?
|
|
151
|
+
!@errors.empty?
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Check if parser has any warnings
|
|
155
|
+
#
|
|
156
|
+
# @return [Boolean] true if there are parsing warnings
|
|
157
|
+
def has_warnings?
|
|
158
|
+
!@warnings.empty?
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Get all parsing errors
|
|
162
|
+
#
|
|
163
|
+
# @return [Array<String>] List of error messages
|
|
164
|
+
def errors
|
|
165
|
+
@errors.dup
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Get all parsing warnings
|
|
169
|
+
#
|
|
170
|
+
# @return [Array<String>] List of warning messages
|
|
171
|
+
def warnings
|
|
172
|
+
@warnings.dup
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Get parsing statistics
|
|
176
|
+
#
|
|
177
|
+
# @return [Hash] Statistics about the parsing process
|
|
178
|
+
def statistics
|
|
179
|
+
stats = @stats_mutex.synchronize { @parse_stats.dup }
|
|
180
|
+
durations = stats[:durations]
|
|
181
|
+
{
|
|
182
|
+
format: format_name,
|
|
183
|
+
errors: @errors.size,
|
|
184
|
+
warnings: @warnings.size,
|
|
185
|
+
options: @options,
|
|
186
|
+
total_parses: stats[:total_parses],
|
|
187
|
+
successful_parses: stats[:successful_parses],
|
|
188
|
+
failed_parses: stats[:failed_parses],
|
|
189
|
+
success_rate: calculate_success_rate(stats),
|
|
190
|
+
average_duration: calculate_average_duration(durations),
|
|
191
|
+
total_duration: stats[:total_duration],
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Reset all parsing statistics
|
|
196
|
+
#
|
|
197
|
+
# @return [void]
|
|
198
|
+
def reset_statistics
|
|
199
|
+
@stats_mutex.synchronize do
|
|
200
|
+
@parse_stats = {
|
|
201
|
+
total_parses: 0,
|
|
202
|
+
successful_parses: 0,
|
|
203
|
+
failed_parses: 0,
|
|
204
|
+
total_duration: 0,
|
|
205
|
+
durations: [],
|
|
206
|
+
}
|
|
207
|
+
end
|
|
208
|
+
@last_duration = nil
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Core parsing implementation
|
|
212
|
+
#
|
|
213
|
+
# @param file_path [String] Path to the file to parse
|
|
214
|
+
# @return [Lutaml::Uml::Document] Parsed UML document
|
|
215
|
+
# @abstract Implement in subclass
|
|
216
|
+
def parse_internal(file_path)
|
|
217
|
+
raise NotImplementedError, "Subclasses must implement #parse_internal"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Hook called before parsing starts
|
|
221
|
+
#
|
|
222
|
+
# @param file_path [String] Path to the file being parsed
|
|
223
|
+
# @return [void]
|
|
224
|
+
def before_parse(file_path)
|
|
225
|
+
# Default implementation does nothing
|
|
226
|
+
# Subclasses can override for pre-processing
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Hook called after parsing completes
|
|
230
|
+
#
|
|
231
|
+
# @param document [Lutaml::Uml::Document] Parsed document
|
|
232
|
+
# @param file_path [String] Path to the source file
|
|
233
|
+
# @return [Lutaml::Uml::Document] Processed document
|
|
234
|
+
def after_parse(document, _file_path)
|
|
235
|
+
# Default implementation returns document unchanged
|
|
236
|
+
# Subclasses can override for post-processing
|
|
237
|
+
document
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Get default parsing options
|
|
241
|
+
#
|
|
242
|
+
# @return [Hash] Default options hash
|
|
243
|
+
def default_options
|
|
244
|
+
{
|
|
245
|
+
validate_input: true,
|
|
246
|
+
validate_output: false,
|
|
247
|
+
include_diagrams: true,
|
|
248
|
+
preserve_ids: true,
|
|
249
|
+
resolve_references: true,
|
|
250
|
+
strict_mode: false,
|
|
251
|
+
}
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Add an error message
|
|
255
|
+
#
|
|
256
|
+
# @param message [String] Error message
|
|
257
|
+
# @param context [Hash] Additional context information
|
|
258
|
+
# @return [void]
|
|
259
|
+
def add_error(message, context = {})
|
|
260
|
+
error_entry = {
|
|
261
|
+
message: message,
|
|
262
|
+
context: context,
|
|
263
|
+
timestamp: Time.now,
|
|
264
|
+
}
|
|
265
|
+
@errors << error_entry
|
|
266
|
+
|
|
267
|
+
# Log error if configuration allows
|
|
268
|
+
log_error(error_entry) if should_log_errors?
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Add a warning message
|
|
272
|
+
#
|
|
273
|
+
# @param message [String] Warning message
|
|
274
|
+
# @param context [Hash] Additional context information
|
|
275
|
+
# @return [void]
|
|
276
|
+
def add_warning(message, context = {})
|
|
277
|
+
warning_entry = {
|
|
278
|
+
message: message,
|
|
279
|
+
context: context,
|
|
280
|
+
timestamp: Time.now,
|
|
281
|
+
}
|
|
282
|
+
@warnings << warning_entry
|
|
283
|
+
|
|
284
|
+
# Log warning if configuration allows
|
|
285
|
+
log_warning(warning_entry) if should_log_errors?
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def add_info(message, context = {})
|
|
289
|
+
add_warning(message, context)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def assign_if_supported(target, setter, value)
|
|
293
|
+
target.public_send(setter, value)
|
|
294
|
+
rescue NoMethodError
|
|
295
|
+
nil
|
|
296
|
+
end
|
|
297
|
+
def format_file_size(size)
|
|
298
|
+
units = %w[B KB MB GB]
|
|
299
|
+
size = size.to_f
|
|
300
|
+
unit_index = 0
|
|
301
|
+
|
|
302
|
+
while size >= 1024 && unit_index < units.length - 1
|
|
303
|
+
size /= 1024
|
|
304
|
+
unit_index += 1
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
"#{size.round(1)} #{units[unit_index]}"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def format_statistics(stats)
|
|
311
|
+
parts = stats.map { |key, value| "#{value} #{key}" }
|
|
312
|
+
parts.join(", ")
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Check if input validation is enabled
|
|
316
|
+
#
|
|
317
|
+
# @return [Boolean] true if input should be validated
|
|
318
|
+
def should_validate_input?
|
|
319
|
+
@options[:validate_input]
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Check if output validation is enabled
|
|
323
|
+
#
|
|
324
|
+
# @return [Boolean] true if output should be validated
|
|
325
|
+
def should_validate_output?
|
|
326
|
+
@options[:validate_output] ||
|
|
327
|
+
@configuration&.transformation_options&.validate_output
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Check if errors should be logged
|
|
331
|
+
#
|
|
332
|
+
# @return [Boolean] true if errors should be logged
|
|
333
|
+
def should_log_errors?
|
|
334
|
+
@configuration&.error_handling&.log_errors != false
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Check if parser should fail fast on errors
|
|
338
|
+
#
|
|
339
|
+
# @return [Boolean] true if parser should fail on first error
|
|
340
|
+
def should_fail_fast?
|
|
341
|
+
@configuration&.error_handling&.fail_fast == true
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
public
|
|
346
|
+
|
|
347
|
+
def validate_file!(file_path)
|
|
348
|
+
if file_path.nil? || file_path.empty?
|
|
349
|
+
raise ArgumentError, "File path cannot be nil"
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
unless File.exist?(file_path)
|
|
353
|
+
raise ArgumentError, "File does not exist: \#{file_path}"
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
unless File.readable?(file_path)
|
|
357
|
+
raise ArgumentError, "File is not readable: \#{file_path}"
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def clear_errors_and_warnings
|
|
362
|
+
@errors.clear
|
|
363
|
+
@warnings.clear
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def validate_output!(document)
|
|
367
|
+
unless document.is_a?(Lutaml::Uml::Document)
|
|
368
|
+
raise ArgumentError, "Parser must return a Lutaml::Uml::Document"
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
if document.packages.nil? && document.classes.nil?
|
|
372
|
+
add_warning("Document contains no packages or classes")
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
private
|
|
377
|
+
|
|
378
|
+
# Calculate success rate percentage
|
|
379
|
+
#
|
|
380
|
+
# @param stats [Hash] Stats hash
|
|
381
|
+
# @return [Float] Success rate as percentage
|
|
382
|
+
def calculate_success_rate(stats)
|
|
383
|
+
return 0.0 if stats[:total_parses].zero?
|
|
384
|
+
|
|
385
|
+
(stats[:successful_parses].to_f / stats[:total_parses]) * 100.0
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Calculate average parse duration
|
|
389
|
+
#
|
|
390
|
+
# @param durations [Array<Float>] List of parse durations
|
|
391
|
+
# @return [Float] Average duration
|
|
392
|
+
def calculate_average_duration(durations)
|
|
393
|
+
return 0.0 if durations.empty?
|
|
394
|
+
|
|
395
|
+
durations.sum / durations.size
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Handle parsing errors according to configuration
|
|
399
|
+
#
|
|
400
|
+
# @param error [StandardError] The error that occurred
|
|
401
|
+
# @param file_path [String] Path to the file being parsed
|
|
402
|
+
# @raise [ParseError] Wrapped parsing error
|
|
403
|
+
def handle_parsing_error(error, file_path) # rubocop:disable Metrics/MethodLength
|
|
404
|
+
error_context = {
|
|
405
|
+
file_path: file_path,
|
|
406
|
+
parser: self.class.name,
|
|
407
|
+
original_error: error.class.name,
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
add_error("Failed to parse #{file_path}: #{error.message}",
|
|
411
|
+
error_context)
|
|
412
|
+
|
|
413
|
+
# Re-raise as ParseError with additional context
|
|
414
|
+
raise ParseError.new(
|
|
415
|
+
"Parsing failed for #{file_path}",
|
|
416
|
+
original_error: error,
|
|
417
|
+
parser: self,
|
|
418
|
+
file_path: file_path,
|
|
419
|
+
)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Log error entry
|
|
423
|
+
#
|
|
424
|
+
# @param error_entry [Hash] Error entry to log
|
|
425
|
+
# @return [void]
|
|
426
|
+
def log_error(error_entry)
|
|
427
|
+
warn "[#{self.class.name}] ERROR: #{error_entry[:message]}"
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# Log warning entry
|
|
431
|
+
#
|
|
432
|
+
# @param warning_entry [Hash] Warning entry to log
|
|
433
|
+
# @return [void]
|
|
434
|
+
def log_warning(warning_entry)
|
|
435
|
+
warn "[#{self.class.name}] WARNING: #{warning_entry[:message]}"
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# Custom error class for parsing failures
|
|
440
|
+
class ParseError < Ea::Error
|
|
441
|
+
# @return [StandardError] Original error that caused parsing failure
|
|
442
|
+
attr_reader :original_error
|
|
443
|
+
|
|
444
|
+
# @return [BaseParser] Parser instance that failed
|
|
445
|
+
attr_reader :parser
|
|
446
|
+
|
|
447
|
+
# @return [String] Path to file that failed to parse
|
|
448
|
+
attr_reader :file_path
|
|
449
|
+
|
|
450
|
+
# Initialize parsing error
|
|
451
|
+
#
|
|
452
|
+
# @param message [String] Error message
|
|
453
|
+
# @param original_error [StandardError] Original error
|
|
454
|
+
# @param parser [BaseParser] Parser that failed
|
|
455
|
+
# @param file_path [String] File that failed to parse
|
|
456
|
+
def initialize(
|
|
457
|
+
message, original_error: nil, parser: nil,
|
|
458
|
+
file_path: nil
|
|
459
|
+
)
|
|
460
|
+
super(message)
|
|
461
|
+
@original_error = original_error
|
|
462
|
+
@parser = parser
|
|
463
|
+
@file_path = file_path
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Get detailed error information
|
|
467
|
+
#
|
|
468
|
+
# @return [Hash] Error details
|
|
469
|
+
def details # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
470
|
+
{
|
|
471
|
+
message: message,
|
|
472
|
+
file_path: @file_path,
|
|
473
|
+
parser: @parser&.class&.name,
|
|
474
|
+
original_error: @original_error&.class&.name,
|
|
475
|
+
original_message: @original_error&.message,
|
|
476
|
+
backtrace: @original_error&.backtrace&.first(5),
|
|
477
|
+
}
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
end
|