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,560 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Diagram
5
+ # API for extracting and rendering UML diagrams from repositories
6
+ #
7
+ # This class provides programmatic access to diagram extraction and
8
+ # rendering functionality. It follows API-first architecture, with
9
+ # all business logic in this class rather than in CLI layer.
10
+ #
11
+ # @example Extract single diagram
12
+ # extractor = DiagramExtractor.new
13
+ # result = extractor.extract_one(
14
+ # "model.lur",
15
+ # "diagram001",
16
+ # output: "diagram.svg"
17
+ # )
18
+ #
19
+ # @example List all diagrams
20
+ # diagrams = extractor.list_diagrams("model.lur")
21
+ # diagrams.each { |d| puts "#{d[:name]} (#{d[:type]})" }
22
+ #
23
+ # @example Batch extraction
24
+ # results = extractor.extract_batch(
25
+ # "model.lur",
26
+ # ["dia1", "dia2", "dia3"],
27
+ # output_dir: "diagrams/"
28
+ # )
29
+ class Extractor
30
+ # Default rendering options
31
+ DEFAULT_OPTIONS = {
32
+ format: "svg",
33
+ padding: 20,
34
+ background_color: "#ffffff",
35
+ grid_visible: false,
36
+ interactive: false,
37
+ config_path: nil,
38
+ }.freeze
39
+
40
+ attr_reader :options
41
+
42
+ # Initialize extractor with options
43
+ #
44
+ # @param options [Hash] Extraction options
45
+ # @option options [Integer] :padding Padding around diagram
46
+ # @option options [String] :background_color Background color
47
+ # @option options [Boolean] :grid_visible Show grid lines
48
+ # @option options [Boolean] :interactive Enable interactivity
49
+ # @option options [String] :config_path Path to diagram config
50
+ # @option options [#all_diagrams,#find_diagram,#classes_index,
51
+ # #packages_index,#associations_index,#document] :repository
52
+ # Repository object for diagram extraction. If not provided,
53
+ # callers must supply repository to each method call.
54
+ def initialize(options = {})
55
+ @repository = options.delete(:repository)
56
+ @options = resolve_options(options)
57
+ end
58
+
59
+ # Extract and render a single diagram
60
+ #
61
+ # @param source [String, Object] LUR file path or repository object
62
+ # @param diagram_id [String] Diagram ID or name
63
+ # @param opts [Hash] Additional options
64
+ # @option opts [String] :output Output file path
65
+ # @return [Hash] Result with :success, :path, :diagram, :message
66
+ def extract_one(source, diagram_id, opts = {}) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
67
+ merged_opts = @options.merge(opts)
68
+ repository = resolve_repository(source)
69
+
70
+ # Find diagram
71
+ diagram = find_diagram(repository, diagram_id)
72
+ unless diagram
73
+ return {
74
+ success: false,
75
+ message: "Diagram not found: #{diagram_id}",
76
+ available: repository.all_diagrams.map(&:name),
77
+ }
78
+ end
79
+
80
+ # Convert to rendering format
81
+ diagram_data = convert_to_rendering_format(diagram, repository)
82
+
83
+ # Render
84
+ svg_content = render_diagram(diagram_data, merged_opts)
85
+
86
+ # Determine output path
87
+ output_path = merged_opts[:output]
88
+
89
+ # Write file if output path specified
90
+ File.write(output_path, svg_content) if output_path
91
+
92
+ result = {
93
+ success: true,
94
+ diagram: diagram_info(diagram),
95
+ format: merged_opts[:format],
96
+ message: "Diagram rendered successfully",
97
+ }
98
+
99
+ # Include path if file was written
100
+ result[:path] = output_path if output_path
101
+
102
+ # Include SVG content if no output file (for testing)
103
+ result[:svg_content] = svg_content unless output_path
104
+
105
+ result
106
+ rescue StandardError => e
107
+ {
108
+ success: false,
109
+ message: "Failed to extract diagram: #{e.message}",
110
+ error: e,
111
+ }
112
+ end
113
+
114
+ # List all diagrams in repository
115
+ #
116
+ # @param source [String, Object] LUR file path or repository object
117
+ # @return [Hash] Result with :success, :diagrams, :count, :message
118
+ def list_diagrams(source = nil) # rubocop:disable Metrics/MethodLength
119
+ repository = resolve_repository(source)
120
+ diagrams = repository.all_diagrams
121
+
122
+ {
123
+ success: true,
124
+ count: diagrams.size,
125
+ diagrams: diagrams.map { |d| diagram_info(d) },
126
+ }
127
+ rescue StandardError => e
128
+ {
129
+ success: false,
130
+ message: "Failed to list diagrams: #{e.message}",
131
+ error: e,
132
+ }
133
+ end
134
+
135
+ # Extract multiple diagrams in batch
136
+ #
137
+ # @param source [String, Object] LUR file path or repository object
138
+ # @param diagram_ids [Array<String>] Array of diagram IDs
139
+ # @param opts [Hash] Additional options
140
+ # @option opts [String] :output_dir Output directory
141
+ # @return [Hash] Result with :success, :results, :summary
142
+ def extract_batch(source, diagram_ids, opts = {}) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
143
+ merged_opts = @options.merge(opts)
144
+ output_dir = merged_opts[:output_dir] || "."
145
+
146
+ # Create output directory if needed
147
+ FileUtils.mkdir_p(output_dir)
148
+
149
+ results = diagram_ids.map do |diagram_id|
150
+ output_path = File.join(output_dir,
151
+ "#{sanitize_filename(diagram_id)}.svg")
152
+ extract_one(source, diagram_id,
153
+ merged_opts.merge(output: output_path))
154
+ end
155
+
156
+ successful = results.count { |r| r[:success] }
157
+ failed = results.count { |r| !r[:success] }
158
+
159
+ {
160
+ success: failed.zero?,
161
+ results: results,
162
+ summary: {
163
+ total: diagram_ids.size,
164
+ successful: successful,
165
+ failed: failed,
166
+ },
167
+ }
168
+ rescue StandardError => e
169
+ {
170
+ success: false,
171
+ message: "Batch extraction failed: #{e.message}",
172
+ error: e,
173
+ }
174
+ end
175
+
176
+ ENV_OPTION_MAP = {
177
+ "LUTAML_DIAGRAM_PADDING" => %i[padding to_i],
178
+ "LUTAML_DIAGRAM_BG_COLOR" => [:background_color, nil],
179
+ "LUTAML_DIAGRAM_GRID" => %i[grid_visible boolean],
180
+ "LUTAML_DIAGRAM_INTERACTIVE" => %i[interactive boolean],
181
+ "LUTAML_DIAGRAM_CONFIG" => [:config_path, nil],
182
+ }.freeze
183
+
184
+ def resolve_options(opts)
185
+ resolved = DEFAULT_OPTIONS.dup
186
+
187
+ ENV_OPTION_MAP.each do |env_key, (option_key, coercion)|
188
+ env_value = ENV.fetch(env_key, nil)
189
+ next unless env_value
190
+
191
+ resolved[option_key] = coerce_env_value(env_value, coercion)
192
+ end
193
+
194
+ resolved.merge(opts)
195
+ end
196
+
197
+ def coerce_env_value(value, coercion)
198
+ case coercion
199
+ when :to_i then value.to_i
200
+ when :boolean then value == "true"
201
+ else value
202
+ end
203
+ end
204
+
205
+ # Resolve source to a repository object
206
+ #
207
+ # @param source [String, Object, nil] File path, repository, or nil
208
+ # @return [Object] Repository object
209
+ def resolve_repository(source)
210
+ return @repository if source.nil?
211
+ return source unless source.is_a?(String)
212
+
213
+ raise "File not found: #{source}" unless File.exist?(source)
214
+
215
+ begin
216
+ require "lutaml/uml_repository"
217
+ Lutaml::UmlRepository::Repository.from_package(source)
218
+ rescue LoadError
219
+ raise "Cannot load LUR file: lutaml/uml_repository gem is required"
220
+ end
221
+ end
222
+
223
+ # Find diagram by ID or name
224
+ def find_diagram(repository, diagram_id)
225
+ # Try exact match by name first
226
+ diagram = repository.find_diagram(diagram_id)
227
+ return diagram if diagram
228
+
229
+ all_diagrams = repository.all_diagrams
230
+
231
+ # Try exact match by XMI ID
232
+ diagram = all_diagrams.find { |d| d.xmi_id == diagram_id }
233
+ return diagram if diagram
234
+
235
+ # Try partial name match (case-insensitive)
236
+ all_diagrams.find do |d|
237
+ d.name.downcase.include?(diagram_id.downcase)
238
+ end
239
+ end
240
+
241
+ # Convert diagram to rendering format
242
+ def convert_to_rendering_format(diagram, repository)
243
+ element_map = build_element_map(repository)
244
+ elements = build_elements(diagram, element_map)
245
+ connectors = build_connectors(diagram, repository, element_map)
246
+
247
+ # Normalize coordinates to EA SVG format (y-flipped, origin-based)
248
+ normalized = normalize_coordinates(elements, connectors)
249
+
250
+ {
251
+ name: diagram.name,
252
+ elements: normalized[:elements],
253
+ connectors: normalized[:connectors],
254
+ }
255
+ end
256
+
257
+ # Normalize EA coordinates to SVG coordinate system
258
+ # EA uses y-up convention; SVG uses y-down.
259
+ # Also shifts all coordinates so minimum x,y is at padding offset.
260
+ def normalize_coordinates(elements, connectors) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
261
+ if elements.empty?
262
+ return { elements: elements,
263
+ connectors: connectors }
264
+ end
265
+
266
+ padding = 10
267
+
268
+ # Find bounding box in EA coordinate space
269
+ min_left = elements.map { |e| e[:x] }.min
270
+ max_top = elements.map { |e| e[:y] }.max
271
+
272
+ # In EA: y increases upward, top > bottom, height = top - bottom
273
+ # For SVG: flip y so y increases downward
274
+ # After negation, the element with max EA y maps to the smallest SVG y.
275
+ # Shift so that smallest SVG y maps to padding.
276
+ x_offset = min_left - padding
277
+ y_offset = -max_top - padding
278
+
279
+ normalized_elements = elements.map do |e|
280
+ e.merge(
281
+ x: e[:x] - x_offset,
282
+ y: -e[:y] - y_offset,
283
+ )
284
+ end
285
+
286
+ normalized_connectors = connectors.map do |c|
287
+ c = c.dup
288
+ if c[:source_element]
289
+ src = c[:source_element].dup
290
+ src[:x] = src[:x] - x_offset
291
+ src[:y] = -src[:y] - y_offset
292
+ c[:source_element] = src
293
+ end
294
+ if c[:target_element]
295
+ tgt = c[:target_element].dup
296
+ tgt[:x] = tgt[:x] - x_offset
297
+ tgt[:y] = -tgt[:y] - y_offset
298
+ c[:target_element] = tgt
299
+ end
300
+ c
301
+ end
302
+
303
+ { elements: normalized_elements, connectors: normalized_connectors }
304
+ end
305
+
306
+ # Build comprehensive element map keyed by XMI ID
307
+ # Handles classes, packages, instances, and EA prefix normalization
308
+ def build_element_map(repository) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
309
+ map = {}
310
+ repository.classes_index.each { |c| map[c.xmi_id] = c }
311
+ repository.packages_index.each { |p| map[p.xmi_id] = p }
312
+
313
+ # Collect instances from packages recursively
314
+ document = repository.document
315
+ document.packages&.each { |pkg| collect_instances(pkg, map) }
316
+
317
+ # Add EA prefix-normalized entries (EAID_ <-> EAPK_ etc.)
318
+ prefix_normalized = {}
319
+ map.each do |xmi_id, element|
320
+ guid = ea_guid(xmi_id)
321
+ prefix_normalized["EAID_#{guid}"] = element
322
+ prefix_normalized["EAPK_#{guid}"] = element
323
+ end
324
+ map.merge!(prefix_normalized)
325
+
326
+ map
327
+ end
328
+
329
+ # Extract GUID portion from EA XMI ID (strip EAID_, EAPK_ prefix)
330
+ def ea_guid(xmi_id)
331
+ xmi_id.sub(/\A(EAID|EAPK)_/, "")
332
+ end
333
+
334
+ # Recursively collect instances from packages
335
+ def collect_instances(pkg, map)
336
+ pkg.instances&.each { |i| map[i.xmi_id] = i }
337
+ pkg.packages&.each { |p| collect_instances(p, map) }
338
+ end
339
+
340
+ # Build element data from diagram objects
341
+ def build_elements(diagram, element_map) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
342
+ diagram.diagram_objects.filter_map do |obj|
343
+ uml_element = element_map[obj.object_xmi_id]
344
+ next unless uml_element
345
+
346
+ element_data = {
347
+ id: obj.diagram_object_id || obj.object_xmi_id,
348
+ type: element_type(uml_element),
349
+ name: uml_element.name,
350
+ x: obj.left || 0,
351
+ y: obj.top || 0,
352
+ width: ((obj.right || 0) - (obj.left || 0)).abs.nonzero? || 120,
353
+ height: ((obj.bottom || 0) - (obj.top || 0)).abs.nonzero? || 80,
354
+ style: obj.style,
355
+ }
356
+
357
+ # Add stereotype
358
+ if uml_element.stereotype
359
+ element_data[:stereotype] =
360
+ array_value(uml_element.stereotype).first
361
+ end
362
+
363
+ # Add class-specific data
364
+ if element_data[:type] == "class"
365
+ add_class_data(element_data,
366
+ uml_element)
367
+ end
368
+
369
+ element_data
370
+ end
371
+ end
372
+
373
+ # Build connector data from diagram links
374
+ def build_connectors(diagram, repository, element_map) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
375
+ diagram.diagram_links.filter_map do |link| # rubocop:disable Metrics/BlockLength
376
+ connector = find_connector(link.connector_xmi_id, repository)
377
+ next unless connector
378
+
379
+ if connector.owner_end_xmi_id
380
+ source_obj = find_diagram_object_by_element(
381
+ connector.owner_end_xmi_id, diagram, element_map
382
+ )
383
+ end
384
+ if connector.member_end_xmi_id
385
+ target_obj = find_diagram_object_by_element(
386
+ connector.member_end_xmi_id, diagram, element_map
387
+ )
388
+ end
389
+
390
+ connector_data = {
391
+ id: link.connector_id || link.connector_xmi_id,
392
+ type: connector_type(connector),
393
+ element: connector,
394
+ diagram_link: link,
395
+ style: link.style,
396
+ geometry: link.geometry,
397
+ path: link.path,
398
+ }
399
+
400
+ # Add source/target positions
401
+ if source_obj
402
+ connector_data[:source_element] =
403
+ diagram_object_bounds(source_obj)
404
+ end
405
+
406
+ if target_obj
407
+ connector_data[:target_element] =
408
+ diagram_object_bounds(target_obj)
409
+ end
410
+
411
+ # Add role and multiplicity
412
+ add_connector_metadata(connector_data, connector)
413
+
414
+ connector_data
415
+ end
416
+ end
417
+
418
+ # Add class attributes and operations
419
+ def add_class_data(element_data, uml_element) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
420
+ if uml_element.attributes
421
+ element_data[:attributes] = uml_element.attributes.map do |attr|
422
+ {
423
+ name: attr.name,
424
+ type: attr.type,
425
+ visibility: attr.visibility || "public",
426
+ }
427
+ end
428
+ end
429
+
430
+ if uml_element.operations
431
+ element_data[:operations] = uml_element.operations.map do |op|
432
+ {
433
+ name: op.name,
434
+ return_type: op.return_type,
435
+ visibility: op.visibility || "public",
436
+ parameters: op.parameters&.map do |p|
437
+ { name: p.name, type: p.type }
438
+ end || [],
439
+ }
440
+ end
441
+ end
442
+ end
443
+
444
+ # Add connector role and multiplicity information
445
+ def add_connector_metadata(connector_data, connector)
446
+ assign_if_present(connector_data, :source_role,
447
+ connector.owner_end_attribute_name)
448
+ assign_if_present(connector_data, :target_role,
449
+ connector.member_end_attribute_name)
450
+ assign_if_present(connector_data, :source_multiplicity,
451
+ connector.owner_end_cardinality, :format)
452
+ assign_if_present(connector_data, :target_multiplicity,
453
+ connector.member_end_cardinality, :format)
454
+ end
455
+
456
+ def assign_if_present(hash, key, value, transform = nil)
457
+ return unless value
458
+
459
+ hash[key] = transform == :format ? format_cardinality(value) : value
460
+ end
461
+
462
+ # Find UML element by XMI ID
463
+ def find_element(xmi_id, repository)
464
+ repository.classes_index.find { |c| c.xmi_id == xmi_id } ||
465
+ repository.packages_index.find { |p| p.xmi_id == xmi_id }
466
+ end
467
+
468
+ # Find connector by XMI ID
469
+ def find_connector(xmi_id, repository)
470
+ repository.associations_index.find { |a| a.xmi_id == xmi_id }
471
+ end
472
+
473
+ # Find diagram object for element, with EA prefix normalization
474
+ def find_diagram_object_by_element(element_xmi_id, diagram, element_map)
475
+ # The element_map has normalized keys, find the original XMI ID
476
+ element = element_map[element_xmi_id]
477
+ return nil unless element
478
+
479
+ # Find the diagram object that references this element
480
+ diagram.diagram_objects.find do |obj|
481
+ obj.object_xmi_id == element_xmi_id ||
482
+ element_map[obj.object_xmi_id] == element
483
+ end
484
+ end
485
+
486
+ # Convert diagram object bounds to x/y/width/height format
487
+ def diagram_object_bounds(obj)
488
+ left = obj.left || 0
489
+ top = obj.top || 0
490
+ right = obj.right || (left + 120)
491
+ bottom = obj.bottom || (top + 80)
492
+ {
493
+ x: left,
494
+ y: top,
495
+ width: (right - left).abs,
496
+ height: (bottom - top).abs,
497
+ }
498
+ end
499
+
500
+ # Determine element type from UML element
501
+ def element_type(uml_element)
502
+ case uml_element
503
+ when Lutaml::Uml::UmlClass then "class"
504
+ when Lutaml::Uml::Package then "package"
505
+ when Lutaml::Uml::DataType then "datatype"
506
+ when Lutaml::Uml::Enum then "enumeration"
507
+ when Lutaml::Uml::Instance then "instance"
508
+ else "unknown"
509
+ end
510
+ end
511
+
512
+ # Determine connector type
513
+ def connector_type(connector)
514
+ case connector
515
+ when Lutaml::Uml::Association then "association"
516
+ when Lutaml::Uml::Generalization then "generalization"
517
+ when Lutaml::Uml::Dependency then "dependency"
518
+ else "connector"
519
+ end
520
+ end
521
+
522
+ # Render diagram to SVG
523
+ def render_diagram(diagram_data, opts)
524
+ Ea::Diagram.render(diagram_data, opts)
525
+ end
526
+
527
+ # Get diagram information
528
+ def diagram_info(diagram)
529
+ {
530
+ xmi_id: diagram.xmi_id,
531
+ name: diagram.name,
532
+ type: diagram.diagram_type,
533
+ package: diagram.package_name || "Unknown",
534
+ objects: diagram.diagram_objects&.size || 0,
535
+ links: diagram.diagram_links&.size || 0,
536
+ }
537
+ end
538
+
539
+ # Default output path for diagram
540
+ def default_output_path(diagram)
541
+ "#{sanitize_filename(diagram.name)}.svg"
542
+ end
543
+
544
+ # Sanitize filename
545
+ def sanitize_filename(name)
546
+ name.gsub(/[^a-zA-Z0-9_-]/, "_")
547
+ end
548
+
549
+ # Format cardinality for display
550
+ def format_cardinality(cardinality)
551
+ cardinality.to_s
552
+ end
553
+
554
+ # Convert value to array
555
+ def array_value(value)
556
+ value.is_a?(Array) ? value : [value]
557
+ end
558
+ end
559
+ end
560
+ end