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