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,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